From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../formautofill/test/browser/address/browser.ini | 13 + .../address/browser_address_doorhanger_display.js | 240 ++++ .../browser/address/browser_address_telemetry.js | 691 +++++++++++ .../test/browser/address/head_address.js | 1 + .../formautofill/test/browser/browser.ini | 35 + .../test/browser/browser_autocomplete_footer.js | 137 +++ .../browser_autocomplete_marked_back_forward.js | 66 + .../browser_autocomplete_marked_detached_tab.js | 58 + .../browser/browser_autofill_address_select.js | 64 + .../browser/browser_autofill_duplicate_fields.js | 95 ++ .../test/browser/browser_check_installed.js | 12 + .../test/browser/browser_dropdown_layout.js | 53 + .../test/browser/browser_editAddressDialog.js | 951 ++++++++++++++ .../formautofill/test/browser/browser_fathom_cc.js | 204 +++ .../browser/browser_first_time_use_doorhanger.js | 142 +++ .../test/browser/browser_manageAddressesDialog.js | 105 ++ .../test/browser/browser_privacyPreferences.js | 439 +++++++ .../test/browser/browser_remoteiframe.js | 127 ++ .../browser/browser_submission_in_private_mode.js | 37 + .../test/browser/browser_update_doorhanger.js | 189 +++ .../test/browser/creditCard/browser.ini | 51 + .../creditCard/browser_anti_clickjacking.js | 123 ++ .../browser_creditCard_doorhanger_action.js | 170 +++ .../browser_creditCard_doorhanger_display.js | 311 +++++ .../browser_creditCard_doorhanger_fields.js | 198 +++ .../browser_creditCard_doorhanger_iframe.js | 103 ++ .../browser_creditCard_doorhanger_logo.js | 238 ++++ .../browser_creditCard_doorhanger_sync.js | 117 ++ .../browser_creditCard_dropdown_layout.js | 57 + .../browser_creditCard_fill_cancel_login.js | 37 + .../creditCard/browser_creditCard_heuristics.js | 165 +++ .../browser_creditCard_heuristics_cc_type.js | 77 ++ ...rowser_creditCard_submission_autodetect_type.js | 104 ++ .../browser_creditCard_submission_normalized.js | 109 ++ .../creditCard/browser_creditCard_telemetry.js | 872 +++++++++++++ .../creditCard/browser_editCreditCardDialog.js | 422 +++++++ .../browser/creditCard/browser_insecure_form.js | 145 +++ .../creditCard/browser_manageCreditCardsDialog.js | 290 +++++ .../test/browser/creditCard/head_cc.js | 1 + .../formautofill/test/browser/empty.html | 8 + .../formautofill/test/browser/fathom/test-setup.sh | 39 + .../browser/fathom/testing/resources/sample/1.svg | 3 + .../browser/fathom/testing/resources/sample/10.svg | 1 + .../browser/fathom/testing/resources/sample/11.png | Bin 0 -> 4968 bytes .../browser/fathom/testing/resources/sample/12.gif | Bin 0 -> 37 bytes .../browser/fathom/testing/resources/sample/13.svg | 16 + .../browser/fathom/testing/resources/sample/14.svg | 14 + .../browser/fathom/testing/resources/sample/15.svg | 1 + .../browser/fathom/testing/resources/sample/16.svg | 11 + .../browser/fathom/testing/resources/sample/17.bin | Bin 0 -> 9594 bytes .../browser/fathom/testing/resources/sample/18.svg | 1 + .../browser/fathom/testing/resources/sample/2.svg | 8 + .../browser/fathom/testing/resources/sample/3.svg | 1 + .../browser/fathom/testing/resources/sample/4.svg | 6 + .../browser/fathom/testing/resources/sample/5.svg | 6 + .../browser/fathom/testing/resources/sample/6.svg | 8 + .../fathom/testing/resources/sample/7.woff2 | Bin 0 -> 15480 bytes .../fathom/testing/resources/sample/8.woff2 | Bin 0 -> 15784 bytes .../fathom/testing/resources/sample/9.woff2 | Bin 0 -> 15908 bytes .../test/browser/fathom/testing/sample.html | 20 + .../test/browser/focus-leak/browser.ini | 12 + .../browser_iframe_typecontent_input_focus.js | 56 + .../doc_iframe_typecontent_input_focus.xhtml | 7 + .../doc_iframe_typecontent_input_focus_frame.html | 6 + .../extensions/formautofill/test/browser/head.js | 1094 ++++++++++++++++ .../test/browser/heuristics/browser.ini | 17 + .../heuristics/browser_autocomplete_off_on_form.js | 74 ++ .../browser_autocomplete_off_on_inputs.js | 102 ++ .../test/browser/heuristics/browser_basic.js | 69 ++ .../test/browser/heuristics/browser_cc_exp.js | 56 + .../test/browser/heuristics/browser_de_fields.js | 32 + .../test/browser/heuristics/browser_fr_fields.js | 27 + .../heuristics/browser_ignore_invisible_fields.js | 115 ++ .../browser/heuristics/browser_multiple_section.js | 118 ++ .../heuristics/browser_parseAddressFields.js | 138 +++ .../browser_section_validation_address.js | 79 ++ .../browser/heuristics/browser_sections_by_name.js | 318 +++++ .../browser/heuristics/third_party/browser.ini | 22 + .../heuristics/third_party/browser_BestBuy.js | 82 ++ .../browser/heuristics/third_party/browser_CDW.js | 71 ++ .../heuristics/third_party/browser_CostCo.js | 170 +++ .../heuristics/third_party/browser_DirectAsda.js | 25 + .../browser/heuristics/third_party/browser_Ebay.js | 25 + .../third_party/browser_GlobalDirectAsda.js | 24 + .../heuristics/third_party/browser_HomeDepot.js | 77 ++ .../heuristics/third_party/browser_Lufthansa.js | 28 + .../browser/heuristics/third_party/browser_Lush.js | 31 + .../heuristics/third_party/browser_Macys.js | 88 ++ .../heuristics/third_party/browser_NewEgg.js | 109 ++ .../heuristics/third_party/browser_OfficeDepot.js | 83 ++ .../browser/heuristics/third_party/browser_QVC.js | 96 ++ .../heuristics/third_party/browser_Sears.js | 81 ++ .../heuristics/third_party/browser_Staples.js | 78 ++ .../heuristics/third_party/browser_Walmart.js | 93 ++ .../test/fixtures/autocomplete_address_basic.html | 26 + .../test/fixtures/autocomplete_basic.html | 52 + .../fixtures/autocomplete_creditcard_basic.html | 29 + .../autocomplete_creditcard_cc_exp_field.html | 28 + .../fixtures/autocomplete_creditcard_iframe.html | 12 + .../test/fixtures/autocomplete_iframe.html | 13 + .../test/fixtures/autocomplete_off_on_form.html | 52 + .../test/fixtures/autocomplete_off_on_inputs.html | 77 ++ .../test/fixtures/autocomplete_simple_basic.html | 19 + .../test/fixtures/heuristics_cc_exp.html | 73 ++ .../test/fixtures/heuristics_de_fields.html | 122 ++ .../test/fixtures/heuristics_fr_fields.html | 34 + .../test/fixtures/multiple_section.html | 84 ++ .../third_party/BestBuy/Checkout_Payment.html | 283 +++++ .../BestBuy/Checkout_ShippingAddress.html | 326 +++++ .../test/fixtures/third_party/BestBuy/SignIn.html | 21 + .../CDW/Checkout_BillingPaymentInfo.html | 469 +++++++ .../fixtures/third_party/CDW/Checkout_Logon.html | 118 ++ .../third_party/CDW/Checkout_ShippingInfo.html | 376 ++++++ .../test/fixtures/third_party/CostCo/Payment.html | 892 ++++++++++++++ .../third_party/CostCo/ShippingAddress.html | 527 ++++++++ .../test/fixtures/third_party/CostCo/SignIn.html | 374 ++++++ .../fixtures/third_party/DirectAsda/Payment.html | 90 ++ .../third_party/Ebay/Checkout_Payment_FR.html | 135 ++ .../third_party/GlobalDirectAsda/Payment.html | 154 +++ .../HomeDepot/Checkout_ShippingPayment.html | 381 ++++++ .../fixtures/third_party/HomeDepot/SignIn.html | 83 ++ .../third_party/Lufthansa/Checkout_Payment.html | 23 + .../test/fixtures/third_party/Lush/index.html | 421 +++++++ .../third_party/Macys/Checkout_Payment.html | 474 +++++++ .../Macys/Checkout_ShippingAddress.html | 439 +++++++ .../test/fixtures/third_party/Macys/SignIn.html | 208 ++++ .../fixtures/third_party/NewEgg/BillingInfo.html | 1074 ++++++++++++++++ .../test/fixtures/third_party/NewEgg/Login.html | 156 +++ .../fixtures/third_party/NewEgg/ShippingInfo.html | 270 ++++ .../fixtures/third_party/OfficeDepot/Payment.html | 672 ++++++++++ .../third_party/OfficeDepot/ShippingAddress.html | 347 ++++++ .../fixtures/third_party/OfficeDepot/SignIn.html | 44 + .../fixtures/third_party/QVC/PaymentMethod.html | 527 ++++++++ .../test/fixtures/third_party/QVC/SignIn.html | 80 ++ .../fixtures/third_party/QVC/YourInformation.html | 522 ++++++++ .../formautofill/test/fixtures/third_party/README | 4 + .../fixtures/third_party/Sears/PaymentOptions.html | 566 +++++++++ .../third_party/Sears/ShippingAddress.html | 447 +++++++ .../test/fixtures/third_party/Staples/Basic.html | 117 ++ .../fixtures/third_party/Staples/Basic_ac_on.html | 117 ++ .../third_party/Staples/PaymentBilling.html | 99 ++ .../third_party/Staples/PaymentBilling_ac_on.html | 98 ++ .../fixtures/third_party/Walmart/Checkout.html | 243 ++++ .../test/fixtures/third_party/Walmart/Payment.html | 235 ++++ .../fixtures/third_party/Walmart/Shipping.html | 234 ++++ .../without_autocomplete_address_basic.html | 26 + .../without_autocomplete_creditcard_basic.html | 53 + .../test/mochitest/creditCard/mochitest.ini | 26 + .../test_basic_creditcard_autocomplete_form.html | 251 ++++ .../test/mochitest/creditCard/test_clear_form.html | 205 +++ .../test_clear_form_expiry_select_elements.html | 211 ++++ .../test_creditcard_autocomplete_off.html | 96 ++ ...w_highlight_with_multiple_cc_number_fields.html | 174 +++ .../test_preview_highlight_with_site_prefill.html | 110 ++ .../test/mochitest/formautofill_common.js | 478 +++++++ .../test/mochitest/formautofill_parent_utils.js | 304 +++++ .../formautofill/test/mochitest/mochitest.ini | 23 + .../mochitest/test_address_level_1_submission.html | 102 ++ .../mochitest/test_autofill_and_ordinal_forms.html | 116 ++ .../test/mochitest/test_autofocus_form.html | 69 ++ .../mochitest/test_basic_autocomplete_form.html | 220 ++++ .../test/mochitest/test_form_changes.html | 128 ++ .../test_formautofill_preview_highlight.html | 121 ++ .../test_multi_locale_CA_address_form.html | 273 ++++ .../test/mochitest/test_multiple_forms.html | 67 + .../test/mochitest/test_on_address_submission.html | 121 ++ browser/extensions/formautofill/test/unit/head.js | 357 ++++++ .../test/unit/head_addressComponent.js | 69 ++ .../formautofill/test/unit/test_activeStatus.js | 176 +++ .../test/unit/test_addressComponent_city.js | 27 + .../test/unit/test_addressComponent_country.js | 47 + .../test/unit/test_addressComponent_email.js | 74 ++ .../test/unit/test_addressComponent_name.js | 101 ++ .../unit/test_addressComponent_organization.js | 55 + .../test/unit/test_addressComponent_postal_code.js | 57 + .../test/unit/test_addressComponent_state.js | 32 + .../unit/test_addressComponent_street_address.js | 56 + .../test/unit/test_addressComponent_tel.js | 76 ++ .../test/unit/test_addressDataLoader.js | 102 ++ .../formautofill/test/unit/test_addressRecords.js | 858 +++++++++++++ .../test/unit/test_autofillFormFields.js | 1078 ++++++++++++++++ .../test/unit/test_clearPopulatedForm.js | 116 ++ .../test/unit/test_collectFormFields.js | 638 ++++++++++ .../formautofill/test/unit/test_createRecords.js | 525 ++++++++ .../test/unit/test_creditCardRecords.js | 926 ++++++++++++++ .../test/unit/test_extractLabelStrings.js | 77 ++ .../test/unit/test_findLabelElements.js | 100 ++ .../test/unit/test_getAdaptedProfiles.js | 1300 ++++++++++++++++++++ .../test/unit/test_getAdaptedProfiles_locales.js | 272 ++++ .../test/unit/test_getCategoriesFromFieldNames.js | 95 ++ .../test/unit/test_getCreditCardLogo.js | 25 + .../test/unit/test_getFormInputDetails.js | 204 +++ .../formautofill/test/unit/test_getInfo.js | 363 ++++++ .../formautofill/test/unit/test_getRecords.js | 258 ++++ .../test/unit/test_isAddressAutofillAvailable.js | 74 ++ .../formautofill/test/unit/test_isCJKName.js | 80 ++ .../unit/test_isCreditCardAutofillAvailable.js | 84 ++ .../unit/test_isCreditCardOrAddressFieldType.js | 103 ++ .../formautofill/test/unit/test_known_strings.js | 148 +++ .../test/unit/test_markAsAutofillField.js | 201 +++ .../formautofill/test/unit/test_migrateRecords.js | 382 ++++++ .../formautofill/test/unit/test_nameUtils.js | 289 +++++ .../formautofill/test/unit/test_onFormSubmitted.js | 805 ++++++++++++ .../test/unit/test_parseAddressFormat.js | 66 + .../test/unit/test_parseStreetAddress.js | 74 ++ .../formautofill/test/unit/test_phoneNumber.js | 399 ++++++ .../test/unit/test_previewFormFields.js | 199 +++ .../test/unit/test_profileAutocompleteResult.js | 450 +++++++ .../formautofill/test/unit/test_reconcile.js | 1173 ++++++++++++++++++ .../formautofill/test/unit/test_savedFieldNames.js | 106 ++ .../formautofill/test/unit/test_storage_remove.js | 88 ++ .../test/unit/test_storage_syncfields.js | 498 ++++++++ .../test/unit/test_storage_tombstones.js | 190 +++ .../extensions/formautofill/test/unit/test_sync.js | 1017 +++++++++++++++ .../unit/test_sync_deprecate_credit_card_v4.js | 248 ++++ .../test/unit/test_toOneLineAddress.js | 64 + .../formautofill/test/unit/test_transformFields.js | 972 +++++++++++++++ .../extensions/formautofill/test/unit/xpcshell.ini | 100 ++ 218 files changed, 42210 insertions(+) create mode 100644 browser/extensions/formautofill/test/browser/address/browser.ini create mode 100644 browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js create mode 100644 browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js create mode 100644 browser/extensions/formautofill/test/browser/address/head_address.js create mode 100644 browser/extensions/formautofill/test/browser/browser.ini create mode 100644 browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js create mode 100644 browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js create mode 100644 browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js create mode 100644 browser/extensions/formautofill/test/browser/browser_autofill_address_select.js create mode 100644 browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js create mode 100644 browser/extensions/formautofill/test/browser/browser_check_installed.js create mode 100644 browser/extensions/formautofill/test/browser/browser_dropdown_layout.js create mode 100644 browser/extensions/formautofill/test/browser/browser_editAddressDialog.js create mode 100644 browser/extensions/formautofill/test/browser/browser_fathom_cc.js create mode 100644 browser/extensions/formautofill/test/browser/browser_first_time_use_doorhanger.js create mode 100644 browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js create mode 100644 browser/extensions/formautofill/test/browser/browser_privacyPreferences.js create mode 100644 browser/extensions/formautofill/test/browser/browser_remoteiframe.js create mode 100644 browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js create mode 100644 browser/extensions/formautofill/test/browser/browser_update_doorhanger.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser.ini create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js create mode 100644 browser/extensions/formautofill/test/browser/creditCard/head_cc.js create mode 100644 browser/extensions/formautofill/test/browser/empty.html create mode 100755 browser/extensions/formautofill/test/browser/fathom/test-setup.sh create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2 create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2 create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2 create mode 100644 browser/extensions/formautofill/test/browser/fathom/testing/sample.html create mode 100644 browser/extensions/formautofill/test/browser/focus-leak/browser.ini create mode 100644 browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js create mode 100644 browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml create mode 100644 browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html create mode 100644 browser/extensions/formautofill/test/browser/head.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser.ini create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_basic.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_ignore_invisible_fields.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_parseAddressFields.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser.ini create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js create mode 100644 browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_basic.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_cc_exp_field.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_iframe.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_iframe.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_off_on_form.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_off_on_inputs.html create mode 100644 browser/extensions/formautofill/test/fixtures/autocomplete_simple_basic.html create mode 100644 browser/extensions/formautofill/test/fixtures/heuristics_cc_exp.html create mode 100644 browser/extensions/formautofill/test/fixtures/heuristics_de_fields.html create mode 100644 browser/extensions/formautofill/test/fixtures/heuristics_fr_fields.html create mode 100644 browser/extensions/formautofill/test/fixtures/multiple_section.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_ShippingAddress.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/BestBuy/SignIn.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_BillingPaymentInfo.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_Logon.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_ShippingInfo.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/CostCo/Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/CostCo/ShippingAddress.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/CostCo/SignIn.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/DirectAsda/Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Ebay/Checkout_Payment_FR.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/GlobalDirectAsda/Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/Checkout_ShippingPayment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/SignIn.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Lufthansa/Checkout_Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Lush/index.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_ShippingAddress.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Macys/SignIn.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/NewEgg/Login.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/NewEgg/ShippingInfo.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/ShippingAddress.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/SignIn.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/QVC/PaymentMethod.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/QVC/SignIn.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/QVC/YourInformation.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/README create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Sears/PaymentOptions.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Sears/ShippingAddress.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic_ac_on.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling_ac_on.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Walmart/Checkout.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Walmart/Payment.html create mode 100644 browser/extensions/formautofill/test/fixtures/third_party/Walmart/Shipping.html create mode 100644 browser/extensions/formautofill/test/fixtures/without_autocomplete_address_basic.html create mode 100644 browser/extensions/formautofill/test/fixtures/without_autocomplete_creditcard_basic.html create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html create mode 100644 browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html create mode 100644 browser/extensions/formautofill/test/mochitest/formautofill_common.js create mode 100644 browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js create mode 100644 browser/extensions/formautofill/test/mochitest/mochitest.ini create mode 100644 browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_autofocus_form.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_form_changes.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_multiple_forms.html create mode 100644 browser/extensions/formautofill/test/mochitest/test_on_address_submission.html create mode 100644 browser/extensions/formautofill/test/unit/head.js create mode 100644 browser/extensions/formautofill/test/unit/head_addressComponent.js create mode 100644 browser/extensions/formautofill/test/unit/test_activeStatus.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_city.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_country.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_email.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_name.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_organization.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_postal_code.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_state.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_street_address.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressComponent_tel.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressDataLoader.js create mode 100644 browser/extensions/formautofill/test/unit/test_addressRecords.js create mode 100644 browser/extensions/formautofill/test/unit/test_autofillFormFields.js create mode 100644 browser/extensions/formautofill/test/unit/test_clearPopulatedForm.js create mode 100644 browser/extensions/formautofill/test/unit/test_collectFormFields.js create mode 100644 browser/extensions/formautofill/test/unit/test_createRecords.js create mode 100644 browser/extensions/formautofill/test/unit/test_creditCardRecords.js create mode 100644 browser/extensions/formautofill/test/unit/test_extractLabelStrings.js create mode 100644 browser/extensions/formautofill/test/unit/test_findLabelElements.js create mode 100644 browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js create mode 100644 browser/extensions/formautofill/test/unit/test_getAdaptedProfiles_locales.js create mode 100644 browser/extensions/formautofill/test/unit/test_getCategoriesFromFieldNames.js create mode 100644 browser/extensions/formautofill/test/unit/test_getCreditCardLogo.js create mode 100644 browser/extensions/formautofill/test/unit/test_getFormInputDetails.js create mode 100644 browser/extensions/formautofill/test/unit/test_getInfo.js create mode 100644 browser/extensions/formautofill/test/unit/test_getRecords.js create mode 100644 browser/extensions/formautofill/test/unit/test_isAddressAutofillAvailable.js create mode 100644 browser/extensions/formautofill/test/unit/test_isCJKName.js create mode 100644 browser/extensions/formautofill/test/unit/test_isCreditCardAutofillAvailable.js create mode 100644 browser/extensions/formautofill/test/unit/test_isCreditCardOrAddressFieldType.js create mode 100644 browser/extensions/formautofill/test/unit/test_known_strings.js create mode 100644 browser/extensions/formautofill/test/unit/test_markAsAutofillField.js create mode 100644 browser/extensions/formautofill/test/unit/test_migrateRecords.js create mode 100644 browser/extensions/formautofill/test/unit/test_nameUtils.js create mode 100644 browser/extensions/formautofill/test/unit/test_onFormSubmitted.js create mode 100644 browser/extensions/formautofill/test/unit/test_parseAddressFormat.js create mode 100644 browser/extensions/formautofill/test/unit/test_parseStreetAddress.js create mode 100644 browser/extensions/formautofill/test/unit/test_phoneNumber.js create mode 100644 browser/extensions/formautofill/test/unit/test_previewFormFields.js create mode 100644 browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js create mode 100644 browser/extensions/formautofill/test/unit/test_reconcile.js create mode 100644 browser/extensions/formautofill/test/unit/test_savedFieldNames.js create mode 100644 browser/extensions/formautofill/test/unit/test_storage_remove.js create mode 100644 browser/extensions/formautofill/test/unit/test_storage_syncfields.js create mode 100644 browser/extensions/formautofill/test/unit/test_storage_tombstones.js create mode 100644 browser/extensions/formautofill/test/unit/test_sync.js create mode 100644 browser/extensions/formautofill/test/unit/test_sync_deprecate_credit_card_v4.js create mode 100644 browser/extensions/formautofill/test/unit/test_toOneLineAddress.js create mode 100644 browser/extensions/formautofill/test/unit/test_transformFields.js create mode 100644 browser/extensions/formautofill/test/unit/xpcshell.ini (limited to 'browser/extensions/formautofill/test') diff --git a/browser/extensions/formautofill/test/browser/address/browser.ini b/browser/extensions/formautofill/test/browser/address/browser.ini new file mode 100644 index 0000000000..4eb6cc8927 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +prefs = + extensions.formautofill.addresses.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 + ../../fixtures/autocomplete_address_basic.html + ../../fixtures/without_autocomplete_address_basic.html + head_address.js + +[browser_address_doorhanger_display.js] +[browser_address_telemetry.js] diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js new file mode 100644 index 0000000000..970051667a --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js @@ -0,0 +1,240 @@ +"use strict"; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.addresses.capture.v2.enabled", true]], + }); +}); + +add_task(async function test_save_doorhanger_shown_no_profile() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(1); + await removeAllRecords(); +}); + +add_task(async function test_save_doorhanger_shown_different_address() { + await setStorage(TEST_ADDRESS_1); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_2["give-name"], + "#street-address": TEST_ADDRESS_2["street-address"], + "#country": TEST_ADDRESS_2.country, + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(2); + await removeAllRecords(); +}); + +add_task( + async function test_update_doorhanger_shown_change_non_mergeable_given_name() { + await setStorage(TEST_ADDRESS_1); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "Cena", + "#street-address": TEST_ADDRESS_1["street-address"], + "#country": TEST_ADDRESS_1.country, + "#email": TEST_ADDRESS_1.email, + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(2); + await removeAllRecords(); + } +); + +add_task(async function test_update_doorhanger_shown_add_email_field() { + // TEST_ADDRESS_2 doesn't contain email field + await setStorage(TEST_ADDRESS_2); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_2["given-name"], + "#street-address": TEST_ADDRESS_2["street-address"], + "#country": TEST_ADDRESS_2.country, + "#email": "test@mozilla.org", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + const addresses = await expectSavedAddresses(1); + is(addresses[0].email, "test@mozilla.org", "Email field is saved"); + + await removeAllRecords(); +}); + +add_task(async function test_doorhanger_not_shown_when_autofill_untouched() { + await setStorage(TEST_ADDRESS_2); + await expectSavedAddresses(1); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await sleep(1000); + await openPopupOn(browser, "form #given-name"); + await sleep(1000); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await sleep(1000); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + "#given-name", + TEST_ADDRESS_2["given-name"] + ); + + 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; + + const addresses = await expectSavedAddresses(1); + is(addresses[0].timesUsed, 1, "timesUsed field set to 1"); + await removeAllRecords(); +}); + +add_task(async function test_doorhanger_not_shown_when_fill_duplicate() { + await setStorage(TEST_ADDRESS_4); + await expectSavedAddresses(1); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_4["given-name"], + "#family-name": TEST_ADDRESS_4["family-name"], + "#organization": TEST_ADDRESS_4.organization, + "#country": TEST_ADDRESS_4.country, + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + const addresses = await expectSavedAddresses(1); + is( + addresses[0]["given-name"], + TEST_ADDRESS_4["given-name"], + "Verify the name field" + ); + is(addresses[0].timesUsed, 1, "timesUsed field set to 1"); + await removeAllRecords(); +}); + +add_task( + async function test_doorhanger_not_shown_when_autofill_then_fill_everything_duplicate() { + await setStorage(TEST_ADDRESS_2, TEST_ADDRESS_3); + await expectSavedAddresses(2); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await openPopupOn(browser, "form #given-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + "#given-name", + TEST_ADDRESS_2["given-name"] + ); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + // Change number to the second credit card number + "#given-name": TEST_ADDRESS_3["given-name"], + "#street-address": TEST_ADDRESS_3["street-address"], + "#postal-code": TEST_ADDRESS_3["postal-code"], + "#country": "", + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + await expectSavedAddresses(2); + await removeAllRecords(); + } +); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js b/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js new file mode 100644 index 0000000000..66dbc24951 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js @@ -0,0 +1,691 @@ +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { AddressTelemetry } = ChromeUtils.import( + "resource://autofill/AutofillTelemetry.jsm" +); + +// Preference definitions +const ENABLED_PREF = ENABLED_AUTOFILL_ADDRESSES_PREF; +const AVAILABLE_PREF = AUTOFILL_ADDRESSES_AVAILABLE_PREF; +const CAPTURE_ENABLE_PREF = ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF; + +// Telemetry definitions +const EVENT_CATEGORY = "address"; + +const SCALAR_DETECTED_SECTION_COUNT = + "formautofill.addresses.detected_sections_count"; +const SCALAR_SUBMITTED_SECTION_COUNT = + "formautofill.addresses.submitted_sections_count"; +const SCALAR_AUTOFILL_PROFILE_COUNT = + "formautofill.addresses.autofill_profiles_count"; + +const HISTOGRAM_PROFILE_NUM_USES = "AUTOFILL_PROFILE_NUM_USES"; +const HISTOGRAM_PROFILE_NUM_USES_KEY = "address"; + +// Autofill UI +const MANAGE_DIALOG_URL = MANAGE_ADDRESSES_DIALOG_URL; +const EDIT_DIALOG_URL = EDIT_ADDRESS_DIALOG_URL; +const DIALOG_SIZE = "width=600,height=400"; +const MANAGE_RECORD_SELECTOR = "#addresses"; + +// Test specific definitions +const TEST_PROFILE = TEST_ADDRESS_1; +const TEST_PROFILE_1 = TEST_ADDRESS_1; +const TEST_PROFILE_2 = TEST_ADDRESS_2; +const TEST_PROFILE_3 = TEST_ADDRESS_3; + +const TEST_FOCUS_NAME_FIELD = "given-name"; +const TEST_FOCUS_NAME_FIELD_SELECTOR = "#" + TEST_FOCUS_NAME_FIELD; + +// Used for tests that update address fields after filling +const TEST_NEW_VALUES = { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", +}; + +const TEST_UPDATE_PROFILE_2_VALUES = { + "#email": "profile2@mozilla.org", +}; +const TEST_BASIC_ADDRESS_FORM_URL = ADDRESS_FORM_URL; +const TEST_BASIC_ADDRESS_FORM_WITHOUT_AC_URL = + ADDRESS_FORM_WITHOUT_AUTOCOMPLETE_URL; +// This should be sync with the address fields that appear in TEST_BASIC_ADDRESS_FORM +const TEST_BASIC_ADDRESS_FORM_FIELDS = [ + "street_address", + "address_level1", + "address_level2", + "postal_code", + "country", + "given_name", + "family_name", + "organization", + "email", + "tel", +]; + +function buildFormExtra(list, fields, fieldValue, defaultValue, aExtra = {}) { + let extra = {}; + for (const field of list) { + if (aExtra[field]) { + extra[field] = aExtra[field]; + } else { + extra[field] = fields.includes(field) ? fieldValue : defaultValue; + } + } + return extra; +} + +/** + * Utility function to generate expected value for `address_form` and `address_form_ext` + * telemetry event. + * + * @param {string} method see `methods` in `address_form` event telemetry + * @param {object} defaultExtra default extra object, this will not be overwritten + * @param {object} fields address fields that will be set to `value` param + * @param {string} value value to set for fields list in `fields` argument + * @param {string} defaultValue value to set for address fields that are not listed in `fields` argument` + */ +function formArgs( + method, + defaultExtra, + fields = [], + value = undefined, + defaultValue = null +) { + if (["popup_shown", "filled_modified"].includes(method)) { + return [["address", method, "address_form", undefined, defaultExtra]]; + } + let extra = buildFormExtra( + AddressTelemetry.SUPPORTED_FIELDS_IN_FORM, + fields, + value, + defaultValue, + defaultExtra + ); + + let extraExt = buildFormExtra( + AddressTelemetry.SUPPORTED_FIELDS_IN_FORM_EXT, + fields, + value, + defaultValue, + defaultExtra + ); + + // The order here should sync with AutofillTelemetry. + return [ + ["address", method, "address_form", undefined, extra], + ["address", method, "address_form_ext", undefined, extraExt], + ]; +} + +function getProfiles() { + return getAddresses(); +} + +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: EVENT_CATEGORY, + }, + { 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: EVENT_CATEGORY, + }, + { process: "parent" } + ); + } +} + +function _assertHistogram(snapshot, expectedNonZeroRanges) { + // Compute the actual ranges in the format { range1: value1, range2: value2 }. + let actualNonZeroRanges = {}; + for (let [range, value] of Object.entries(snapshot.values)) { + if (value > 0) { + actualNonZeroRanges[range] = value; + } + } + + // These are stringified to visualize the differences between the values. + Assert.equal( + JSON.stringify(actualNonZeroRanges), + JSON.stringify(expectedNonZeroRanges) + ); +} + +function assertKeyedHistogram(histogramId, key, expected) { + let snapshot = Services.telemetry + .getKeyedHistogramById(histogramId) + .snapshot(); + + if (expected == undefined) { + Assert.deepEqual(snapshot, {}); + } else { + _assertHistogram(snapshot[key], expected); + } +} + +async function openTabAndUseAutofillProfile( + idx, + profile, + { closeTab = true, submitForm = true } = {} +) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_BASIC_ADDRESS_FORM_URL + ); + let browser = tab.linkedBrowser; + + await openPopupOn(browser, "form " + TEST_FOCUS_NAME_FIELD_SELECTOR); + + for (let i = 0; i <= idx; i++) { + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + } + + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + TEST_FOCUS_NAME_FIELD_SELECTOR, + profile[TEST_FOCUS_NAME_FIELD] + ); + await focusUpdateSubmitForm( + browser, + { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: {}, + }, + submitForm + ); + + if (!closeTab) { + return tab; + } + + await BrowserTestUtils.removeTab(tab); + return null; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_PREF, true], + [AVAILABLE_PREF, "on"], + [CAPTURE_ENABLE_PREF, true], + ], + }); + + Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + registerCleanupFunction(() => { + Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false); + }); +}); + +add_task(async function test_popup_opened() { + await setStorage(TEST_PROFILE); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + const focusInput = TEST_FOCUS_NAME_FIELD_SELECTOR; + + await openPopupOn(browser, focusInput); + + // Clean up + await closePopup(browser); + } + ); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + await assertTelemetry([ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT, + 1 + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_popup_opened_form_without_autocomplete() { + await setStorage(TEST_PROFILE); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_WITHOUT_AC_URL }, + async function (browser) { + const focusInput = TEST_FOCUS_NAME_FIELD_SELECTOR; + await openPopupOn(browser, focusInput); + await closePopup(browser); + } + ); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + await assertTelemetry([ + ...formArgs("detected", {}, fields, "0", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_submit_autofill_profile_new() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.firstTimeUse", true]], + }); + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await focusUpdateSubmitForm(browser, { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: TEST_NEW_VALUES, + }); + + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + SCALAR_AUTOFILL_PROFILE_COUNT, + expectChanged, + "There should be ${expectChanged} profile(s) stored." + ); + } + } + ); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + useCount + ); + + await removeAllRecords(); + } + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + let expected_content = [ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("submitted", {}, fields, "user_filled", "unavailable"), + ]; + + // FTU + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "capture_doorhanger"], + [EVENT_CATEGORY, "pref", "capture_doorhanger"], + ]); + + // Need to close preference tab + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 sections detected." + ); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT, + 1, + "There should be 1 section submitted." + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_submit_autofill_profile_update() { + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await setStorage(TEST_PROFILE_2); + let profiles = await getProfiles(); + Assert.equal(profiles.length, 1, "1 entry in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await openPopupOn(browser, TEST_FOCUS_NAME_FIELD_SELECTOR); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + + await waitForAutofill( + browser, + TEST_FOCUS_NAME_FIELD_SELECTOR, + TEST_PROFILE_2[TEST_FOCUS_NAME_FIELD] + ); + await focusUpdateSubmitForm(browser, { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: TEST_UPDATE_PROFILE_2_VALUES, + }); + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + SCALAR_AUTOFILL_PROFILE_COUNT, + expectChanged, + "There should be ${expectChanged} profile(s) stored." + ); + } + } + ); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + useCount + ); + + SpecialPowers.clearUserPref(ENABLED_PREF); + + await removeAllRecords(); + } + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + let expected_content = [ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ...formArgs( + "filled", + { + given_name: "filled", + street_address: "filled", + country: "filled", + }, + fields, + "not_filled", + "unavailable" + ), + ...formArgs( + "submitted", + { + given_name: "autofilled", + street_address: "autofilled", + country: "autofilled", + email: "user_filled", + }, + fields, + "not_filled", + "unavailable" + ), + ]; + + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "update_doorhanger"], + [EVENT_CATEGORY, "update", "update_doorhanger"], + ]); + + await test_per_command(SECONDARY_BUTTON, undefined, { 0: 1, 1: 1 }, 2); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "update_doorhanger"], + [EVENT_CATEGORY, "save", "update_doorhanger"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_removingAutofillProfilesViaKeyboardDelete() { + await setStorage(TEST_PROFILE); + + let win = window.openDialog(MANAGE_DIALOG_URL, null, DIALOG_SIZE); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(MANAGE_RECORD_SELECTOR); + Assert.equal(selRecords.length, 1, "One entry"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeKey("VK_DELETE", {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + Assert.equal(selRecords.length, 0, "No entry left"); + + win.close(); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show", "manage"], + [EVENT_CATEGORY, "delete", "manage"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_saveAutofillProfile() { + Services.telemetry.clearEvents(); + + await testDialog(EDIT_DIALOG_URL, win => { + // TODP: Default to US because the layout will be different + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["given-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["family-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["street-address"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["address-level2"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["postal-code"], {}, win); + info("saving one entry"); + win.document.querySelector("#save").click(); + }); + + await assertTelemetry(undefined, [[EVENT_CATEGORY, "add", "manage"]]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_editAutofillProfile() { + Services.telemetry.clearEvents(); + + await setStorage(TEST_PROFILE); + + let profiles = await getProfiles(); + Assert.equal(profiles.length, 1, "1 entry in storage"); + await testDialog( + EDIT_DIALOG_URL, + win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + win.document.querySelector("#save").click(); + }, + { + record: profiles[0], + } + ); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show_entry", "manage"], + [EVENT_CATEGORY, "edit", "manage"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_histogram() { + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + await setStorage(TEST_PROFILE_1, TEST_PROFILE_2, TEST_PROFILE_3); + let profiles = await getProfiles(); + Assert.equal(profiles.length, 3, "3 entry in storage"); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 3 } + ); + + await openTabAndUseAutofillProfile(0, TEST_PROFILE_1); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 2, 1: 1 } + ); + + await openTabAndUseAutofillProfile(1, TEST_PROFILE_2); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 1: 2 } + ); + + await openTabAndUseAutofillProfile(0, TEST_PROFILE_2); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 1: 1, 2: 1 } + ); + + await openTabAndUseAutofillProfile(1, TEST_PROFILE_1); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 2: 2 } + ); + + await openTabAndUseAutofillProfile(2, TEST_PROFILE_3); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 1: 1, 2: 2 } + ); + + await removeAllRecords(); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + undefined + ); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_clear_autofill_profile_autofill() { + // Address does not have clear pref. Keep the test so we know we should implement + // the test if we support clearing address via autocomplete. + Assert.ok(true); +}); diff --git a/browser/extensions/formautofill/test/browser/address/head_address.js b/browser/extensions/formautofill/test/browser/address/head_address.js new file mode 100644 index 0000000000..42196e8422 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/head_address.js @@ -0,0 +1 @@ +/* import-globals-from ../head.js */ diff --git a/browser/extensions/formautofill/test/browser/browser.ini b/browser/extensions/formautofill/test/browser/browser.ini new file mode 100644 index 0000000000..c9d100430b --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser.ini @@ -0,0 +1,35 @@ +[DEFAULT] +head = head.js +support-files = + ./fathom/** + ../fixtures/autocomplete_basic.html + ../fixtures/autocomplete_iframe.html + ../fixtures/autocomplete_simple_basic.html + ./empty.html + +[browser_autocomplete_footer.js] +skip-if = verify +[browser_autocomplete_marked_back_forward.js] +[browser_autocomplete_marked_detached_tab.js] +skip-if = + verify && (os == "win") + os == "mac" +[browser_autofill_address_select.js] +[browser_autofill_duplicate_fields.js] +[browser_check_installed.js] +[browser_dropdown_layout.js] +[browser_editAddressDialog.js] +skip-if = + verify + win10_2004 # Bug 1723573 + win11_2009 # Bug 1797751 +[browser_fathom_cc.js] +[browser_first_time_use_doorhanger.js] +skip-if = verify +[browser_manageAddressesDialog.js] +[browser_privacyPreferences.js] +skip-if = (( os == "mac") || (os == 'linux') || (os == 'win')) # perma-fail see Bug 1600059 +[browser_remoteiframe.js] +[browser_submission_in_private_mode.js] +[browser_update_doorhanger.js] +skip-if = true # bug 1426981 # Bug 1445538 diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js new file mode 100644 index 0000000000..d2f5c72f16 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js @@ -0,0 +1,137 @@ +"use strict"; + +const URL = BASE_URL + "autocomplete_basic.html"; + +add_task(async function setup_storage() { + await setStorage( + TEST_ADDRESS_2, + TEST_ADDRESS_3, + TEST_ADDRESS_4, + TEST_ADDRESS_5 + ); +}); + +add_task(async function test_press_enter_on_footer() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + const { + autoCompletePopup: { richlistbox: itemsBox }, + } = browser; + + await openPopupOn(browser, "#organization"); + // Navigate to the footer and press enter. + const listItemElems = itemsBox.querySelectorAll( + ".autocomplete-richlistitem" + ); + const prefTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + PRIVACY_PREF_URL, + true + ); + for (let i = 0; i < listItemElems.length; i++) { + if (!listItemElems[i].collapsed) { + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + } + } + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + info(`expecting tab: about:preferences#privacy opened`); + const prefTab = await prefTabPromise; + info(`expecting tab: about:preferences#privacy removed`); + BrowserTestUtils.removeTab(prefTab); + ok( + true, + "Tab: preferences#privacy was successfully opened by pressing enter on the footer" + ); + + await closePopup(browser); + } + ); +}); + +add_task(async function test_click_on_footer() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + const { + autoCompletePopup: { richlistbox: itemsBox }, + } = browser; + + await openPopupOn(browser, "#organization"); + // Click on the footer + let optionButton = itemsBox.querySelector( + ".autocomplete-richlistitem:last-child" + ); + while (optionButton.collapsed) { + optionButton = optionButton.previousElementSibling; + } + optionButton = optionButton._optionButton; + + const prefTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + PRIVACY_PREF_URL, + true + ); + // Make sure dropdown is visible before continuing mouse synthesizing. + await BrowserTestUtils.waitForCondition(() => + BrowserTestUtils.is_visible(optionButton) + ); + await EventUtils.synthesizeMouseAtCenter(optionButton, {}); + info(`expecting tab: about:preferences#privacy opened`); + const prefTab = await prefTabPromise; + info(`expecting tab: about:preferences#privacy removed`); + BrowserTestUtils.removeTab(prefTab); + ok( + true, + "Tab: preferences#privacy was successfully opened by clicking on the footer" + ); + + await closePopup(browser); + } + ); +}); + +add_task(async function test_phishing_warning_single_category() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + const { + autoCompletePopup: { richlistbox: itemsBox }, + } = browser; + + await openPopupOn(browser, "#tel"); + const warningBox = itemsBox.querySelector( + ".autocomplete-richlistitem:last-child" + )._warningTextBox; + ok(warningBox, "Got phishing warning box"); + await expectWarningText(browser, "Autofills phone"); + is( + warningBox.ownerGlobal.getComputedStyle(warningBox).backgroundColor, + "rgba(248, 232, 28, 0.2)", + "Check warning text background color" + ); + + await closePopup(browser); + } + ); +}); + +add_task(async function test_phishing_warning_complex_categories() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + await openPopupOn(browser, "#street-address"); + + await expectWarningText(browser, "Also autofills organization, email"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await expectWarningText(browser, "Autofills address"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await expectWarningText(browser, "Also autofills organization, email"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await expectWarningText(browser, "Also autofills organization, email"); + + await closePopup(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js new file mode 100644 index 0000000000..ada61fa014 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js @@ -0,0 +1,66 @@ +/** + * Test that autofill autocomplete works after back/forward navigation + */ + +"use strict"; + +const URL = BASE_URL + "autocomplete_basic.html"; + +function checkPopup(autoCompletePopup) { + let first = autoCompletePopup.view.results[0]; + const { primary, secondary } = JSON.parse(first.label); + ok( + primary.startsWith(TEST_ADDRESS_1["street-address"].split("\n")[0]), + "Check primary label is street address" + ); + is( + secondary, + TEST_ADDRESS_1["address-level2"], + "Check secondary label is address-level2" + ); +} + +add_task(async function setup_storage() { + await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3); +}); + +add_task(async function test_back_forward() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + const { autoCompletePopup } = browser; + + // Check the page after the initial load + await openPopupOn(browser, "#street-address"); + checkPopup(autoCompletePopup); + + // Now navigate forward and make sure autofill autocomplete results are still attached + let loadPromise = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, `${URL}?load=2`); + info("expecting browser loaded"); + await loadPromise; + + // Check the second page + await openPopupOn(browser, "#street-address"); + checkPopup(autoCompletePopup); + + // Check after hitting back to the first page + let stoppedPromise = BrowserTestUtils.browserStopped(browser); + browser.goBack(); + info("expecting browser stopped"); + await stoppedPromise; + await openPopupOn(browser, "#street-address"); + checkPopup(autoCompletePopup); + + // Check after hitting forward to the second page + stoppedPromise = BrowserTestUtils.browserStopped(browser); + browser.goForward(); + info("expecting browser stopped"); + await stoppedPromise; + await openPopupOn(browser, "#street-address"); + checkPopup(autoCompletePopup); + + await closePopup(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js new file mode 100644 index 0000000000..399e7e16e5 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js @@ -0,0 +1,58 @@ +/** + * Test that autofill autocomplete works after detaching a tab + */ + +"use strict"; + +const URL = BASE_URL + "autocomplete_basic.html"; + +function checkPopup(autoCompletePopup) { + let first = autoCompletePopup.view.results[0]; + const { primary, secondary } = JSON.parse(first.label); + ok( + primary.startsWith(TEST_ADDRESS_1["street-address"].split("\n")[0]), + "Check primary label is street address" + ); + is( + secondary, + TEST_ADDRESS_1["address-level2"], + "Check secondary label is address-level2" + ); +} + +add_task(async function setup_storage() { + await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3); +}); + +add_task(async function test_detach_tab_marked() { + let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, url: URL }); + let browser = tab.linkedBrowser; + const { autoCompletePopup } = browser; + + // Check the page after the initial load + await openPopupOn(browser, "#street-address"); + checkPopup(autoCompletePopup); + await closePopup(browser); + + // Detach the tab to a new window + info("expecting tab replaced with new window"); + let windowLoadedPromise = BrowserTestUtils.waitForNewWindow(); + let newWin = gBrowser.replaceTabWithWindow( + gBrowser.getTabForBrowser(browser) + ); + await windowLoadedPromise; + + info("tab was detached"); + let newBrowser = newWin.gBrowser.selectedBrowser; + ok(newBrowser, "Found new "); + let newAutoCompletePopup = newBrowser.autoCompletePopup; + ok(newAutoCompletePopup, "Found new autocomplete popup"); + + await openPopupOn(newBrowser, "#street-address"); + checkPopup(newAutoCompletePopup); + + await closePopup(newBrowser); + let windowRefocusedPromise = BrowserTestUtils.waitForEvent(window, "focus"); + await BrowserTestUtils.closeWindow(newWin); + await windowRefocusedPromise; +}); diff --git a/browser/extensions/formautofill/test/browser/browser_autofill_address_select.js b/browser/extensions/formautofill/test/browser/browser_autofill_address_select.js new file mode 100644 index 0000000000..68e38ad10d --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_autofill_address_select.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PROFILE_US = { + email: "address_us@mozilla.org", + organization: "Mozilla", + country: "US", +}; + +const TEST_PROFILE_CA = { + email: "address_ca@mozilla.org", + organization: "Mozilla", + country: "CA", +}; + +const MARKUP_SELECT_COUNTRY = ` + + + + + +`; + +add_autofill_heuristic_tests([ + { + fixtureData: MARKUP_SELECT_COUNTRY, + profile: TEST_PROFILE_US, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE_US.email }, + { fieldName: "organization", autofill: TEST_PROFILE_US.organization }, + { fieldName: "country", autofill: "United States" }, + ], + }, + ], + }, + { + fixtureData: MARKUP_SELECT_COUNTRY, + profile: TEST_PROFILE_CA, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE_CA.email }, + { fieldName: "organization", autofill: TEST_PROFILE_CA.organization }, + { fieldName: "country", autofill: "Canada" }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js b/browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js new file mode 100644 index 0000000000..d7f80c2749 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PROFILE = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "Mozilla", + "street-address": "331 E Evelyn Ave", + "address-level2": "Mountain View", + "address-level1": "CA", + "postal-code": "94041", + country: "US", + tel: "+16509030800", + email: "address@mozilla.org", +}; + +add_autofill_heuristic_tests([ + { + fixtureData: ` + + + + + + `, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] }, + { fieldName: "country", autofill: TEST_PROFILE.country }, + ], + }, + ], + }, + { + description: "autofill multiple email fields(2)", + fixtureData: ` + + + + + + + `, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] }, + { fieldName: "country", autofill: TEST_PROFILE.country }, + ], + }, + ], + }, + { + description: "autofill multiple email fields(3)", + fixtureData: ` + + + + + + + + `, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] }, + { fieldName: "country", autofill: TEST_PROFILE.country }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/browser_check_installed.js b/browser/extensions/formautofill/test/browser/browser_check_installed.js new file mode 100644 index 0000000000..a93e64c209 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_check_installed.js @@ -0,0 +1,12 @@ +"use strict"; + +add_task(async function test_enabled() { + let addon = await AddonManager.getAddonByID("formautofill@mozilla.org"); + isnot(addon, null, "Check addon exists"); + is(addon.version, "1.0.1", "Check version"); + is(addon.name, "Form Autofill", "Check name"); + ok(addon.isCompatible, "Check application compatibility"); + ok(!addon.appDisabled, "Check not app disabled"); + ok(addon.isActive, "Check addon is active"); + is(addon.type, "extension", "Check type is 'extension'"); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js b/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js new file mode 100644 index 0000000000..bc1d2fccab --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js @@ -0,0 +1,53 @@ +"use strict"; + +const URL = + "http://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html"; + +add_task(async function setup_storage() { + await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_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_address_dropdown() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + const focusInput = "#organization"; + await openPopupOn(browser, focusInput); + const firstItem = getDisplayedPopupItems(browser)[0]; + + is(firstItem.getAttribute("ac-image"), "", "Should not show icon"); + + // The breakpoint of two-lines layout is 150px + await reopenPopupWithResizedInput(browser, focusInput, 140); + is( + firstItem._itemBox.getAttribute("size"), + "small", + "Show two-lines layout" + ); + await reopenPopupWithResizedInput(browser, focusInput, 160); + is( + firstItem._itemBox.hasAttribute("size"), + false, + "Show one-line layout" + ); + + await closePopup(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js new file mode 100644 index 0000000000..0088f916bd --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js @@ -0,0 +1,951 @@ +"use strict"; + +const { FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Region: "resource://gre/modules/Region.sys.mjs", +}); + +requestLongerTimeout(6); + +add_task(async function setup_supportedCountries() { + await SpecialPowers.pushPrefEnv({ + set: [[SUPPORTED_COUNTRIES_PREF, "US,CA,DE"]], + }); +}); + +add_task(async function test_cancelEditAddressDialog() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, win => { + win.document.querySelector("#cancel").click(); + }); +}); + +add_task(async function test_cancelEditAddressDialogWithESC() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, win => { + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + }); +}); + +add_task(async function test_defaultCountry() { + Region._setHomeRegion("CA", false); + await testDialog(EDIT_ADDRESS_DIALOG_URL, win => { + let doc = win.document; + is( + doc.querySelector("#country").value, + "CA", + "Default country set to Canada" + ); + doc.querySelector("#cancel").click(); + }); + Region._setHomeRegion("DE", false); + await testDialog(EDIT_ADDRESS_DIALOG_URL, win => { + let doc = win.document; + is( + doc.querySelector("#country").value, + "DE", + "Default country set to Germany" + ); + doc.querySelector("#cancel").click(); + }); + // Test unsupported country + Region._setHomeRegion("XX", false); + await testDialog(EDIT_ADDRESS_DIALOG_URL, win => { + let doc = win.document; + is(doc.querySelector("#country").value, "", "Default country set to empty"); + doc.querySelector("#cancel").click(); + }); + Region._setHomeRegion("US", false); +}); + +add_task(async function test_saveAddress() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, win => { + let doc = win.document; + // Verify labels + is( + doc.querySelector("#address-level1-container > .label-text").textContent, + "State", + "US address-level1 label should be 'State'" + ); + is( + doc.querySelector("#postal-code-container > .label-text").textContent, + "ZIP Code", + "US postal-code label should be 'ZIP Code'" + ); + // Input address info and verify move through form with tab keys + let keypresses = [ + "VK_TAB", + TEST_ADDRESS_1["given-name"], + "VK_TAB", + TEST_ADDRESS_1["additional-name"], + "VK_TAB", + TEST_ADDRESS_1["family-name"], + "VK_TAB", + TEST_ADDRESS_1["street-address"], + "VK_TAB", + TEST_ADDRESS_1["address-level2"], + "VK_TAB", + TEST_ADDRESS_1["address-level1"], + "VK_TAB", + TEST_ADDRESS_1["postal-code"], + "VK_TAB", + TEST_ADDRESS_1.organization, + "VK_TAB", + // TEST_ADDRESS_1.country, // Country is already US + "VK_TAB", + TEST_ADDRESS_1.tel, + "VK_TAB", + TEST_ADDRESS_1.email, + "VK_TAB", + ]; + if (AppConstants.platform != "win") { + keypresses.push("VK_TAB", "VK_RETURN"); + } else { + keypresses.push("VK_RETURN"); + } + keypresses.forEach(keypress => { + if ( + doc.activeElement.localName == "select" && + !keypress.startsWith("VK_") + ) { + let field = doc.activeElement; + while (field.value != keypress) { + EventUtils.synthesizeKey(keypress[0], {}, win); + } + } else { + EventUtils.synthesizeKey(keypress, {}, win); + } + }); + }); + let addresses = await getAddresses(); + + is(addresses.length, 1, "only one address is in storage"); + is( + Object.keys(TEST_ADDRESS_1).length, + 11, + "Sanity check number of properties" + ); + for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_1)) { + is(addresses[0][fieldName], fieldValue, "check " + fieldName); + } +}); + +add_task(async function test_editAddress() { + let addresses = await getAddresses(); + await testDialog( + EDIT_ADDRESS_DIALOG_URL, + win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + + let stateSelect = win.document.querySelector("#address-level1"); + is( + stateSelect.selectedOptions[0].value, + TEST_ADDRESS_1["address-level1"], + "address-level1 should be selected in the dropdown" + ); + + win.document.querySelector("#save").click(); + }, + { + record: addresses[0], + } + ); + addresses = await getAddresses(); + + is(addresses.length, 1, "only one address is in storage"); + is( + addresses[0]["given-name"], + TEST_ADDRESS_1["given-name"] + "test", + "given-name changed" + ); + await removeAddresses([addresses[0].guid]); + + addresses = await getAddresses(); + is(addresses.length, 0, "Address storage is empty"); +}); + +add_task( + async function test_editAddressFrenchCanadianChangedToEnglishRepresentation() { + let addressClone = Object.assign({}, TEST_ADDRESS_CA_1); + addressClone["address-level1"] = "Colombie-Britannique"; + await setStorage(addressClone); + + let addresses = await getAddresses(); + await testDialog( + EDIT_ADDRESS_DIALOG_URL, + win => { + let stateSelect = win.document.querySelector("#address-level1"); + is( + stateSelect.selectedOptions[0].value, + "BC", + "address-level1 should have 'BC' selected in the dropdown" + ); + + win.document.querySelector("#save").click(); + }, + { + record: addresses[0], + } + ); + addresses = await getAddresses(); + + is(addresses.length, 1, "only one address is in storage"); + is(addresses[0]["address-level1"], "BC", "address-level1 changed"); + await removeAddresses([addresses[0].guid]); + + addresses = await getAddresses(); + is(addresses.length, 0, "Address storage is empty"); + } +); + +add_task(async function test_editSparseAddress() { + let record = { ...TEST_ADDRESS_1 }; + info("delete some usually required properties"); + delete record["street-address"]; + delete record["address-level1"]; + delete record["address-level2"]; + await testDialog( + EDIT_ADDRESS_DIALOG_URL, + win => { + is( + win.document.querySelectorAll(":-moz-ui-invalid").length, + 0, + "Check no fields are visually invalid" + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + is( + win.document.querySelector("#save").disabled, + false, + "Save button should be enabled after an edit" + ); + win.document.querySelector("#cancel").click(); + }, + { + record, + } + ); +}); + +add_task(async function test_saveAddressCA() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => { + let doc = win.document; + // Change country to verify labels + doc.querySelector("#country").focus(); + EventUtils.synthesizeKey("Canada", {}, win); + + await TestUtils.waitForCondition(() => { + return ( + doc.querySelector("#address-level1-container > .label-text") + .textContent == "Province" + ); + }, "Wait for the mutation observer to change the labels"); + is( + doc.querySelector("#address-level1-container > .label-text").textContent, + "Province", + "CA address-level1 label should be 'Province'" + ); + is( + doc.querySelector("#postal-code-container > .label-text").textContent, + "Postal Code", + "CA postal-code label should be 'Postal Code'" + ); + is( + doc.querySelector("#address-level3-container").style.display, + "none", + "CA address-level3 should be hidden" + ); + + // Input address info and verify move through form with tab keys + doc.querySelector("#given-name").focus(); + let keyInputs = [ + TEST_ADDRESS_CA_1["given-name"], + "VK_TAB", + TEST_ADDRESS_CA_1["additional-name"], + "VK_TAB", + TEST_ADDRESS_CA_1["family-name"], + "VK_TAB", + TEST_ADDRESS_CA_1.organization, + "VK_TAB", + TEST_ADDRESS_CA_1["street-address"], + "VK_TAB", + TEST_ADDRESS_CA_1["address-level2"], + "VK_TAB", + TEST_ADDRESS_CA_1["address-level1"], + "VK_TAB", + TEST_ADDRESS_CA_1["postal-code"], + "VK_TAB", + // TEST_ADDRESS_1.country, // Country is already selected above + "VK_TAB", + TEST_ADDRESS_CA_1.tel, + "VK_TAB", + TEST_ADDRESS_CA_1.email, + "VK_TAB", + ]; + if (AppConstants.platform != "win") { + keyInputs.push("VK_TAB", "VK_RETURN"); + } else { + keyInputs.push("VK_RETURN"); + } + keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win)); + }); + let addresses = await getAddresses(); + for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_CA_1)) { + is(addresses[0][fieldName], fieldValue, "check " + fieldName); + } + await removeAllRecords(); +}); + +add_task(async function test_saveAddressDE() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => { + let doc = win.document; + // Change country to verify labels + doc.querySelector("#country").focus(); + EventUtils.synthesizeKey("Germany", {}, win); + await TestUtils.waitForCondition(() => { + return ( + doc.querySelector("#postal-code-container > .label-text").textContent == + "Postal Code" + ); + }, "Wait for the mutation observer to change the labels"); + is( + doc.querySelector("#postal-code-container > .label-text").textContent, + "Postal Code", + "DE postal-code label should be 'Postal Code'" + ); + is( + doc.querySelector("#address-level1-container").style.display, + "none", + "DE address-level1 should be hidden" + ); + is( + doc.querySelector("#address-level3-container").style.display, + "none", + "DE address-level3 should be hidden" + ); + // Input address info and verify move through form with tab keys + doc.querySelector("#given-name").focus(); + let keyInputs = [ + TEST_ADDRESS_DE_1["given-name"], + "VK_TAB", + TEST_ADDRESS_DE_1["additional-name"], + "VK_TAB", + TEST_ADDRESS_DE_1["family-name"], + "VK_TAB", + TEST_ADDRESS_DE_1.organization, + "VK_TAB", + TEST_ADDRESS_DE_1["street-address"], + "VK_TAB", + TEST_ADDRESS_DE_1["postal-code"], + "VK_TAB", + TEST_ADDRESS_DE_1["address-level2"], + "VK_TAB", + // TEST_ADDRESS_1.country, // Country is already selected above + "VK_TAB", + TEST_ADDRESS_DE_1.tel, + "VK_TAB", + TEST_ADDRESS_DE_1.email, + "VK_TAB", + ]; + if (AppConstants.platform != "win") { + keyInputs.push("VK_TAB", "VK_RETURN"); + } else { + keyInputs.push("VK_RETURN"); + } + keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win)); + }); + let addresses = await getAddresses(); + for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_DE_1)) { + is(addresses[0][fieldName], fieldValue, "check " + fieldName); + } + await removeAllRecords(); +}); + +/** + * Test saving an address for a region from regionNames.properties but not in + * addressReferences.js (libaddressinput). + */ +add_task(async function test_saveAddress_nolibaddressinput() { + const TEST_ADDRESS = { + ...TEST_ADDRESS_IE_1, + ...{ + "address-level3": undefined, + country: "XG", + }, + }; + + isnot( + FormAutofillUtils.getCountryAddressData("XG").key, + "XG", + "Check that the region we're testing with isn't in libaddressinput" + ); + + await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => { + let doc = win.document; + + // Change country to verify labels + doc.querySelector("#country").focus(); + EventUtils.synthesizeKey("Gaza Strip", {}, win); + await TestUtils.waitForCondition(() => { + return ( + doc.querySelector("#postal-code-container > .label-text").textContent == + "Postal Code" + ); + }, "Wait for the mutation observer to change the labels"); + is( + doc.querySelector("#postal-code-container > .label-text").textContent, + "Postal Code", + "XG postal-code label should be 'Postal Code'" + ); + isnot( + doc.querySelector("#address-level1-container").style.display, + "none", + "XG address-level1 should be hidden" + ); + is( + doc.querySelector("#address-level2").localName, + "input", + "XG address-level2 should be an " + ); + // Input address info and verify move through form with tab keys + doc.querySelector("#given-name").focus(); + let keyInputs = [ + TEST_ADDRESS["given-name"], + "VK_TAB", + TEST_ADDRESS["additional-name"], + "VK_TAB", + TEST_ADDRESS["family-name"], + "VK_TAB", + TEST_ADDRESS.organization, + "VK_TAB", + TEST_ADDRESS["street-address"], + "VK_TAB", + TEST_ADDRESS["address-level2"], + "VK_TAB", + TEST_ADDRESS["address-level1"], + "VK_TAB", + TEST_ADDRESS["postal-code"], + "VK_TAB", + // TEST_ADDRESS_1.country, // Country is already selected above + "VK_TAB", + TEST_ADDRESS.tel, + "VK_TAB", + TEST_ADDRESS.email, + "VK_TAB", + ]; + if (AppConstants.platform != "win") { + keyInputs.push("VK_TAB", "VK_RETURN"); + } else { + keyInputs.push("VK_RETURN"); + } + keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win)); + }); + let addresses = await getAddresses(); + for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS)) { + is(addresses[0][fieldName], fieldValue, "check " + fieldName); + } + await removeAllRecords(); +}); + +add_task(async function test_saveAddressIE() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => { + let doc = win.document; + // Change country to verify labels + doc.querySelector("#country").focus(); + EventUtils.synthesizeKey("Ireland", {}, win); + await TestUtils.waitForCondition(() => { + return ( + doc.querySelector("#postal-code-container > .label-text").textContent == + "Eircode" + ); + }, "Wait for the mutation observer to change the labels"); + is( + doc.querySelector("#postal-code-container > .label-text").textContent, + "Eircode", + "IE postal-code label should be 'Eircode'" + ); + is( + doc.querySelector("#address-level1-container > .label-text").textContent, + "County", + "IE address-level1 should be 'County'" + ); + is( + doc.querySelector("#address-level3-container > .label-text").textContent, + "Townland", + "IE address-level3 should be 'Townland'" + ); + + // Input address info and verify move through form with tab keys + doc.querySelector("#given-name").focus(); + let keyInputs = [ + TEST_ADDRESS_IE_1["given-name"], + "VK_TAB", + TEST_ADDRESS_IE_1["additional-name"], + "VK_TAB", + TEST_ADDRESS_IE_1["family-name"], + "VK_TAB", + TEST_ADDRESS_IE_1.organization, + "VK_TAB", + TEST_ADDRESS_IE_1["street-address"], + "VK_TAB", + TEST_ADDRESS_IE_1["address-level3"], + "VK_TAB", + TEST_ADDRESS_IE_1["address-level2"], + "VK_TAB", + TEST_ADDRESS_IE_1["address-level1"], + "VK_TAB", + TEST_ADDRESS_IE_1["postal-code"], + "VK_TAB", + // TEST_ADDRESS_1.country, // Country is already selected above + "VK_TAB", + TEST_ADDRESS_IE_1.tel, + "VK_TAB", + TEST_ADDRESS_IE_1.email, + "VK_TAB", + ]; + if (AppConstants.platform != "win") { + keyInputs.push("VK_TAB", "VK_RETURN"); + } else { + keyInputs.push("VK_RETURN"); + } + keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win)); + }); + + let addresses = await getAddresses(); + for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_IE_1)) { + is(addresses[0][fieldName], fieldValue, "check " + fieldName); + } + await removeAllRecords(); +}); + +add_task(async function test_countryAndStateFieldLabels() { + await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => { + let doc = win.document; + // Change country to verify labels + doc.querySelector("#country").focus(); + + let mutableLabels = [ + "postal-code-container", + "address-level1-container", + "address-level2-container", + "address-level3-container", + ].map(containerID => + doc.getElementById(containerID).querySelector(":scope > .label-text") + ); + + for (let countryOption of doc.querySelector("#country").options) { + if (countryOption.value == "") { + info("Skipping the empty country option"); + continue; + } + + // Clear L10N textContent to not leave leftovers between country tests + for (let labelEl of mutableLabels) { + doc.l10n.setAttributes(labelEl, ""); + labelEl.textContent = ""; + } + + info(`Selecting '${countryOption.label}' (${countryOption.value})`); + EventUtils.synthesizeKey(countryOption.label, {}, win); + + let l10nResolve; + let l10nReady = new Promise(resolve => { + l10nResolve = resolve; + }); + let verifyL10n = () => { + if (mutableLabels.every(labelEl => labelEl.textContent)) { + win.removeEventListener("MozAfterPaint", verifyL10n); + l10nResolve(); + } + }; + win.addEventListener("MozAfterPaint", verifyL10n); + await l10nReady; + + // Check that the labels were filled + for (let labelEl of mutableLabels) { + isnot( + labelEl.textContent, + "", + "Ensure textContent is non-empty for: " + countryOption.value + ); + } + + let stateOptions = doc.querySelector("#address-level1").options; + /* eslint-disable max-len */ + let expectedStateOptions = { + BS: { + // The Bahamas is an interesting testcase because they have some keys that are full names, and others are replaced with ISO IDs. + keys: "Abaco~AK~Andros~BY~BI~CI~Crooked Island~Eleuthera~EX~Grand Bahama~HI~IN~LI~MG~N.P.~RI~RC~SS~SW".split( + "~" + ), + names: + "Abaco Islands~Acklins~Andros Island~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma and Cays~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~New Providence~Ragged Island~Rum Cay~San Salvador~Spanish Wells".split( + "~" + ), + }, + US: { + keys: "AL~AK~AS~AZ~AR~AA~AE~AP~CA~CO~CT~DE~DC~FL~GA~GU~HI~ID~IL~IN~IA~KS~KY~LA~ME~MH~MD~MA~MI~FM~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~MP~OH~OK~OR~PW~PA~PR~RI~SC~SD~TN~TX~UT~VT~VI~VA~WA~WV~WI~WY".split( + "~" + ), + names: + "Alabama~Alaska~American Samoa~Arizona~Arkansas~Armed Forces (AA)~Armed Forces (AE)~Armed Forces (AP)~California~Colorado~Connecticut~Delaware~District of Columbia~Florida~Georgia~Guam~Hawaii~Idaho~Illinois~Indiana~Iowa~Kansas~Kentucky~Louisiana~Maine~Marshall Islands~Maryland~Massachusetts~Michigan~Micronesia~Minnesota~Mississippi~Missouri~Montana~Nebraska~Nevada~New Hampshire~New Jersey~New Mexico~New York~North Carolina~North Dakota~Northern Mariana Islands~Ohio~Oklahoma~Oregon~Palau~Pennsylvania~Puerto Rico~Rhode Island~South Carolina~South Dakota~Tennessee~Texas~Utah~Vermont~Virgin Islands~Virginia~Washington~West Virginia~Wisconsin~Wyoming".split( + "~" + ), + }, + }; + /* eslint-enable max-len */ + + if (expectedStateOptions[countryOption.value]) { + let { keys, names } = expectedStateOptions[countryOption.value]; + is( + stateOptions.length, + keys.length + 1, + "stateOptions should list all options plus a blank entry" + ); + is(stateOptions[0].value, "", "First State option should be blank"); + for (let i = 1; i < stateOptions.length; i++) { + is( + stateOptions[i].value, + keys[i - 1], + "Each State should be listed in alphabetical name order (key)" + ); + is( + stateOptions[i].text, + names[i - 1], + "Each State should be listed in alphabetical name order (name)" + ); + } + } + // Dispatch a dummy key event so that 's incremental search is cleared. + EventUtils.synthesizeKey("VK_ACCEPT", {}, win); + + doc.querySelector("#country").focus(); + EventUtils.synthesizeKey("Romania", {}, win); + + await TestUtils.waitForCondition( + () => { + provinceField = doc.getElementById("address-level1"); + return provinceField.parentNode.style.display == "none"; + }, + "Wait for address-level1 to become hidden", + 10 + ); + + ok( + provinceField.required, + "address-level1 will still be marked as required" + ); + ok(provinceField.disabled, "address-level1 should be marked as disabled"); + + doc.querySelector("#cancel").click(); + }); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_fathom_cc.js b/browser/extensions/formautofill/test/browser/browser_fathom_cc.js new file mode 100644 index 0000000000..2e465fd503 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_fathom_cc.js @@ -0,0 +1,204 @@ +/** + * By default this test only tests 1 sample. This is to avoid publishing all samples we have + * to the codebase. If you update the Fathom CC model, please follow the instruction below + * and run the test. Doing this makes sure the optimized (Native implementation) CC fathom model produces + * exactly the same result as the non-optimized model (JS implementation, See CreditCardRuleset.sys.mjs). + * + * To test this: + * 1. Run the test setup script (fathom/test-setup.sh) to download all samples to the local + * directory. Note that you need to have the permission to access the fathom-form-autofill + * 2. Set `gTestAutofillRepoSample` to true + * 3. Run this test + */ + +"use strict"; + +const eligibleElementSelector = + "input:not([type]), input[type=text], input[type=textbox], input[type=email], input[type=tel], input[type=number], input[type=month], select, button"; + +const skippedSamples = [ + // TOOD: Crash while running the following testcases. Since this is not caused by the fathom CC + // model, we just skip those for now + "EN_B105b.html", + "EN_B312a.html", + "EN_B118b.html", + "EN_B48c.html", + + // This sample is skipped because of Bug 1754256 (Support lookaround regex for native fathom CC implementation). + "DE_B378b.html", +]; + +async function run_test(path, dirs) { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.creditCards.heuristics.mode", 1]], + }); + + // Collect files we are going to test. + let files = []; + for (let dir of dirs) { + let entries = new FileUtils.File(getTestFilePath(path + dir)) + .directoryEntries; + + while (entries.hasMoreElements()) { + let entry = entries.nextFile; + if (skippedSamples.includes(entry.leafName)) { + continue; + } + + if (entry.leafName.endsWith(".html")) { + files.push(path + dir + entry.leafName); + } + } + } + + ok(files.length, "no sample files found"); + + let summary = {}; + for (let file of files) { + info("Testing " + file + "..."); + + await BrowserTestUtils.withNewTab(BASE_URL + file, async browser => { + summary[file] = await SpecialPowers.spawn( + browser, + [{ eligibleElementSelector, file }], + obj => { + const { FormAutofillHeuristics } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs" + ); + const { FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" + ); + + let eligibleFields = []; + let nodeList = content.document.querySelectorAll( + obj.eligibleElementSelector + ); + for (let i = 0; i < nodeList.length; i++) { + if (FormAutofillUtils.isCreditCardOrAddressFieldType(nodeList[i])) { + eligibleFields.push(nodeList[i]); + } + } + let failedFields = []; + + info("Running CC fathom model"); + let nativeConfidencesKeyedByType = + ChromeUtils.getFormAutofillConfidences(eligibleFields); + let jsConfidencesKeyedByType = + FormAutofillHeuristics.getFormAutofillConfidences(eligibleFields); + + if (eligibleFields.length != nativeConfidencesKeyedByType.length) { + ok( + false, + `Get the wrong number of confidence value from the native model` + ); + } + if (eligibleFields.length != jsConfidencesKeyedByType.length) { + ok( + false, + `Get the wrong number of confidence value from the js model` + ); + } + + // This value should sync with the number of supported types in + // CreditCardRuleset.sys.mjs (See `get types()` in `this.creditCardRulesets`). + const EXPECTED_NUM_OF_CONFIDENCE = 2; + for (let i = 0; i < eligibleFields.length; i++) { + if ( + Object.keys(nativeConfidencesKeyedByType[i]).length != + EXPECTED_NUM_OF_CONFIDENCE + ) { + ok( + false, + `Native CC model doesn't get confidence value for all types` + ); + } + if ( + Object.keys(jsConfidencesKeyedByType[i]).length != + EXPECTED_NUM_OF_CONFIDENCE + ) { + ok( + false, + `JS CC model doesn't get confidence value for all types` + ); + } + + for (let [type, confidence] of Object.entries( + nativeConfidencesKeyedByType[i] + )) { + // Fix to 10 digit to ignore rounding error between js and c++. + let nativeConfidence = confidence.toFixed(10); + let jsConfidence = + jsConfidencesKeyedByType[i][ + FormAutofillUtils.formAutofillConfidencesKeyToCCFieldType( + type + ) + ].toFixed(10); + if (jsConfidence != nativeConfidence) { + info( + `${obj.file}: Element(id=${eligibleFields[i].id} doesn't have the same confidence value when rule type is ${type}` + ); + if (!failedFields.includes(i)) { + failedFields.push(i); + } + } + } + } + ok( + !failedFields.length, + `${obj.file}: has the same confidences value on both models` + ); + return { + tested: eligibleFields.length, + passed: eligibleFields.length - failedFields.length, + }; + } + ); + }); + } + + // Generating summary report + let total_tested_samples = 0; + let total_passed_samples = 0; + let total_tested_fields = 0; + let total_passed_fields = 0; + info("=====Summary====="); + for (const [k, v] of Object.entries(summary)) { + total_tested_samples++; + if (v.tested == v.passed) { + total_passed_samples++; + } else { + info("Failed Case:" + k); + } + total_tested_fields += v.tested; + total_passed_fields += v.passed; + } + info( + "Passed Samples/Test Samples: " + + total_passed_samples + + "/" + + total_tested_samples + ); + info( + "Passed Fields/Test Fields: " + + total_passed_fields + + "/" + + total_tested_fields + ); +} + +add_task(async function test_native_cc_model() { + const path = "fathom/"; + const dirs = ["testing/"]; + await run_test(path, dirs); +}); + +add_task(async function test_native_cc_model_autofill_repo() { + const path = "fathom/autofill-repo-samples/"; + const dirs = ["validation/", "training/", "testing/"]; + if (await IOUtils.exists(getTestFilePath(path))) { + // Just to ignore timeout failure while running the test on the local + requestLongerTimeout(10); + + await run_test(path, dirs); + } +}); diff --git a/browser/extensions/formautofill/test/browser/browser_first_time_use_doorhanger.js b/browser/extensions/formautofill/test/browser/browser_first_time_use_doorhanger.js new file mode 100644 index 0000000000..40709eb984 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_first_time_use_doorhanger.js @@ -0,0 +1,142 @@ +"use strict"; + +add_task(async function test_first_time_save() { + let addresses = await getAddresses(); + is(addresses.length, 0, "No address in storage"); + await SpecialPowers.pushPrefEnv({ + set: [ + [FTU_PREF, true], + [ENABLED_AUTOFILL_ADDRESSES_PREF, true], + [AUTOFILL_ADDRESSES_AVAILABLE_PREF, "on"], + [ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF, true], + ], + }); + + let onAdded = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:preferences#privacy" + ); + await focusUpdateSubmitForm(browser, { + focusSelector: "#organization", + newValues: { + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", + }, + }); + + await onPopupShown; + let cb = getDoorhangerCheckbox(); + ok(cb.hidden, "Sync checkbox should be hidden"); + // Open the panel via main button + await clickDoorhangerButton(MAIN_BUTTON); + let tab = await tabPromise; + ok(tab, "Privacy panel opened"); + BrowserTestUtils.removeTab(tab); + } + ); + await onAdded; + + addresses = await getAddresses(); + is(addresses.length, 1, "Address saved"); + let ftuPref = SpecialPowers.getBoolPref(FTU_PREF); + is(ftuPref, false, "First time use flag is false"); +}); + +add_task(async function test_non_first_time_save() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_AUTOFILL_ADDRESSES_PREF, true], + [AUTOFILL_ADDRESSES_AVAILABLE_PREF, "on"], + [ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF, true], + ], + }); + let addresses = await getAddresses(); + let ftuPref = SpecialPowers.getBoolPref(FTU_PREF); + is(ftuPref, false, "First time use flag is false"); + is(addresses.length, 1, "1 address in storage"); + + let onAdded = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#organization", + newValues: { + "#organization": "Mozilla", + "#street-address": "331 E. Evelyn Avenue", + "#tel": "1-650-903-0800", + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onAdded; + + addresses = await getAddresses(); + is(addresses.length, 2, "Another address saved"); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_first_time_save_with_sync_account() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FTU_PREF, true], + [ENABLED_AUTOFILL_ADDRESSES_PREF, true], + [AUTOFILL_ADDRESSES_AVAILABLE_PREF, "on"], + [SYNC_USERNAME_PREF, "foo@bar.com"], + [SYNC_ADDRESSES_PREF, false], + ], + }); + + let onAdded = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:preferences#privacy-address-autofill" + ); + await focusUpdateSubmitForm(browser, { + focusSelector: "#organization", + newValues: { + "#organization": "Foobar", + "#email": "foo@bar.com", + "#tel": "1-234-567-8900", + }, + }); + + await onPopupShown; + let cb = getDoorhangerCheckbox(); + ok(!cb.hidden, "Sync checkbox should be visible"); + + is(cb.checked, false, "Checkbox state should match addresses sync state"); + cb.click(); + is( + SpecialPowers.getBoolPref(SYNC_ADDRESSES_PREF), + true, + "addresses sync should be enabled after checked" + ); + // Open the panel via main button + await clickDoorhangerButton(MAIN_BUTTON); + let tab = await tabPromise; + ok(tab, "Privacy panel opened"); + BrowserTestUtils.removeTab(tab); + } + ); + await onAdded; + + let ftuPref = SpecialPowers.getBoolPref(FTU_PREF); + is(ftuPref, false, "First time use flag is false"); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js b/browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js new file mode 100644 index 0000000000..b070df0fda --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js @@ -0,0 +1,105 @@ +"use strict"; + +const TEST_SELECTORS = { + selRecords: "#addresses", + btnRemove: "#remove", + btnAdd: "#add", + btnEdit: "#edit", +}; + +const DIALOG_SIZE = "width=600,height=400"; + +add_task(async function test_manageAddressesInitialState() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: MANAGE_ADDRESSES_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 btnEdit = content.document.querySelector(args.btnEdit); + let btnAdd = content.document.querySelector(args.btnAdd); + + is(selRecords.length, 0, "No address"); + is(btnAdd.disabled, false, "Add button enabled"); + is(btnRemove.disabled, true, "Remove button disabled"); + is(btnEdit.disabled, true, "Edit button disabled"); + }); + } + ); +}); + +add_task(async function test_cancelManageAddressDialogWithESC() { + let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL); + await waitForFocusAndFormReady(win); + let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + await unloadPromise; + ok(true, "Manage addresses dialog is closed with ESC key"); +}); + +add_task(async function test_removingSingleAndMultipleAddresses() { + await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3); + + let win = window.openDialog(MANAGE_ADDRESSES_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); + + is(selRecords.length, 3, "Three addresses"); + + 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, 2, "Two addresses left"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeMouseAtCenter( + selRecords.children[1], + { 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 addresses are removed"); + + win.close(); +}); + +add_task(async function test_removingAdressViaKeyboardDelete() { + await setStorage(TEST_ADDRESS_1); + let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL, null, DIALOG_SIZE); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + + is(selRecords.length, 1, "One address"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeKey("VK_DELETE", {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + is(selRecords.length, 0, "No addresses left"); + + win.close(); +}); + +add_task(async function test_addressesDialogWatchesStorageChanges() { + let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL, null, DIALOG_SIZE); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + + await setStorage(TEST_ADDRESS_1); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded"); + is(selRecords.length, 1, "One address is shown"); + + await removeAddresses([selRecords.options[0].value]); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded"); + is(selRecords.length, 0, "Address is removed"); + win.close(); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_privacyPreferences.js b/browser/extensions/formautofill/test/browser/browser_privacyPreferences.js new file mode 100644 index 0000000000..2d5759ca66 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_privacyPreferences.js @@ -0,0 +1,439 @@ +"use strict"; + +const PAGE_PREFS = "about:preferences"; +const PAGE_PRIVACY = PAGE_PREFS + "#privacy"; +const SELECTORS = { + group: "#formAutofillGroupBox", + addressAutofillCheckbox: "#addressAutofill checkbox", + creditCardAutofillCheckbox: "#creditCardAutofill checkbox", + savedAddressesBtn: "#addressAutofill button", + savedCreditCardsBtn: "#creditCardAutofill button", + addressAutofillLearnMore: "#addressAutofillLearnMore", + creditCardAutofillLearnMore: "#creditCardAutofillLearnMore", + reauthCheckbox: "#creditCardReauthenticate checkbox", +}; + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +// Visibility of form autofill group should be hidden when opening +// preferences page. +add_task(async function test_aboutPreferences() { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PREFS }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.group).hidden, + true, + "Form Autofill group should be hidden" + ); + }); + } + ); +}); + +// Visibility of form autofill group should be visible when opening +// directly to privacy page. Checkbox is checked by default. +add_task(async function test_aboutPreferencesPrivacy() { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.group).hidden, + false, + "Form Autofill group should be visible" + ); + is( + content.document.querySelector(selectors.addressAutofillCheckbox) + .checked, + true, + "Autofill addresses checkbox should be checked" + ); + is( + content.document.querySelector(selectors.creditCardAutofillCheckbox) + .checked, + true, + "Autofill credit cards checkbox should be checked" + ); + ok( + content.document + .querySelector(selectors.addressAutofillLearnMore) + .href.includes("autofill-card-address"), + "Autofill addresses learn more link should contain autofill-card-address" + ); + ok( + content.document + .querySelector(selectors.creditCardAutofillLearnMore) + .href.includes("credit-card-autofill"), + "Autofill credit cards learn more link should contain credit-card-autofill" + ); + }); + } + ); +}); + +add_task(async function test_openManageAutofillDialogs() { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + const args = [ + SELECTORS, + MANAGE_ADDRESSES_DIALOG_URL, + MANAGE_CREDIT_CARDS_DIALOG_URL, + ]; + await SpecialPowers.spawn( + browser, + [args], + ([selectors, addrUrl, ccUrl]) => { + function testManageDialogOpened(expectedUrl) { + return { + open: openUrl => is(openUrl, expectedUrl, "Manage dialog called"), + }; + } + + let realgSubDialog = content.window.gSubDialog; + content.window.gSubDialog = testManageDialogOpened(addrUrl); + content.document.querySelector(selectors.savedAddressesBtn).click(); + content.window.gSubDialog = testManageDialogOpened(ccUrl); + content.document.querySelector(selectors.savedCreditCardsBtn).click(); + content.window.gSubDialog = realgSubDialog; + } + ); + } + ); +}); + +add_task(async function test_autofillCheckboxes() { + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_ADDRESSES_PREF, false]], + }); + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, false]], + }); + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + // Checkbox should be unchecked when form autofill addresses and credit cards are disabled. + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.group).hidden, + false, + "Form Autofill group should be visible" + ); + is( + content.document.querySelector(selectors.addressAutofillCheckbox) + .checked, + false, + "Checkbox should be unchecked when Autofill Addresses is disabled" + ); + is( + content.document.querySelector(selectors.creditCardAutofillCheckbox) + .checked, + false, + "Checkbox should be unchecked when Autofill Credit Cards is disabled" + ); + content.document + .querySelector(selectors.addressAutofillCheckbox) + .scrollIntoView({ block: "center", behavior: "instant" }); + }); + + info("test toggling the checkboxes"); + await BrowserTestUtils.synthesizeMouseAtCenter( + SELECTORS.addressAutofillCheckbox, + {}, + browser + ); + is( + Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF), + true, + "Check address autofill is now enabled" + ); + + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + content.document + .querySelector(selectors.creditCardAutofillCheckbox) + .scrollIntoView({ block: "center", behavior: "instant" }); + }); + await BrowserTestUtils.synthesizeMouseAtCenter( + SELECTORS.creditCardAutofillCheckbox, + {}, + browser + ); + is( + Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF), + true, + "Check credit card autofill is now enabled" + ); + } + ); +}); + +add_task(async function test_creditCardNotAvailable() { + await SpecialPowers.pushPrefEnv({ + set: [[AUTOFILL_CREDITCARDS_AVAILABLE_PREF, false]], + }); + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.group).hidden, + false, + "Form Autofill group should be visible" + ); + ok( + !content.document.querySelector(selectors.creditCardAutofillCheckbox), + "Autofill credit cards checkbox should not exist" + ); + }); + } + ); +}); + +add_task(async function test_reauth() { + await SpecialPowers.pushPrefEnv({ + set: [[AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"]], + }); + let { OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" + ); + + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn( + browser, + [SELECTORS, OSKeyStore.canReauth()], + (selectors, canReauth) => { + is( + canReauth, + !!content.document.querySelector(selectors.reauthCheckbox), + "Re-authentication checkbox should be available if OSKeyStore.canReauth() is `true`" + ); + } + ); + } + ); +}); + +add_task(async function test_addressAutofillNotAvailable() { + await SpecialPowers.pushPrefEnv({ + set: [[AUTOFILL_ADDRESSES_AVAILABLE_PREF, "off"]], + }); + + let autofillAddressEnabledValue = Services.prefs.getBoolPref( + ENABLED_AUTOFILL_ADDRESSES_PREF + ); + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.group).hidden, + false, + "Form Autofill group should be visible" + ); + is( + content.document.querySelector(selectors.addressAutofillCheckbox), + null, + "Address checkbox should not exist when address autofill is not enabled" + ); + is( + content.document.querySelector(selectors.creditCardAutofillCheckbox) + .checked, + true, + "Checkbox should be checked when Autofill Credit Cards is enabled" + ); + }); + info("test toggling the credit card autofill checkbox"); + + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + content.document + .querySelector(selectors.creditCardAutofillCheckbox) + .scrollIntoView({ block: "center", behavior: "instant" }); + }); + await BrowserTestUtils.synthesizeMouseAtCenter( + SELECTORS.creditCardAutofillCheckbox, + {}, + browser + ); + is( + Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF), + false, + "Check credit card autofill is now disabled" + ); + is( + Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF), + autofillAddressEnabledValue, + "Address autofill enabled's value should not change due to credit card checkbox interaction" + ); + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.addressAutofillCheckbox), + null, + "Address checkbox should exist due to interaction with credit card checkbox" + ); + }); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_addressAutofillNotAvailableViaRegion() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.supported", "detect"], + ["extensions.formautofill.creditCards.enabled", true], + ["browser.search.region", "FR"], + [ENABLED_AUTOFILL_ADDRESSES_SUPPORTED_COUNTRIES_PREF, "US,CA"], + ], + }); + + const addressAutofillEnabledPrefValue = Services.prefs.getBoolPref( + ENABLED_AUTOFILL_ADDRESSES_PREF + ); + const addressAutofillAvailablePrefValue = Services.prefs.getCharPref( + AUTOFILL_ADDRESSES_AVAILABLE_PREF + ); + is( + FormAutofill.isAutofillAddressesAvailable, + false, + "Address autofill should not be available due to unsupported region" + ); + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.group).hidden, + false, + "Form Autofill group should be visible" + ); + is( + content.document.querySelector(selectors.addressAutofillCheckbox), + null, + "Address checkbox should not exist due to address autofill not being available" + ); + is( + content.document.querySelector(selectors.creditCardAutofillCheckbox) + .checked, + true, + "Checkbox should be checked when Autofill Credit Cards is enabled" + ); + }); + info("test toggling the credit card autofill checkbox"); + + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + content.document + .querySelector(selectors.creditCardAutofillCheckbox) + .scrollIntoView({ block: "center", behavior: "instant" }); + }); + await BrowserTestUtils.synthesizeMouseAtCenter( + SELECTORS.creditCardAutofillCheckbox, + {}, + browser + ); + is( + Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF), + false, + "Check credit card autofill is now disabled" + ); + is( + Services.prefs.getCharPref(AUTOFILL_ADDRESSES_AVAILABLE_PREF), + addressAutofillAvailablePrefValue, + "Address autofill availability should not change due to interaction with credit card checkbox" + ); + is( + addressAutofillEnabledPrefValue, + Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF), + "Address autofill enabled pref should not change due to credit card checkbox" + ); + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.addressAutofillCheckbox), + null, + "Address checkbox should not exist due to address autofill not being available" + ); + }); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Checkboxes should be disabled based on whether or not they are locked. +add_task(async function test_aboutPreferencesPrivacy() { + Services.prefs.lockPref(ENABLED_AUTOFILL_ADDRESSES_PREF); + Services.prefs.lockPref(ENABLED_AUTOFILL_CREDITCARDS_PREF); + registerCleanupFunction(function () { + Services.prefs.unlockPref(ENABLED_AUTOFILL_ADDRESSES_PREF); + Services.prefs.unlockPref(ENABLED_AUTOFILL_CREDITCARDS_PREF); + }); + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_PRIVACY }, + async function (browser) { + await finalPrefPaneLoaded; + await SpecialPowers.spawn(browser, [SELECTORS], selectors => { + is( + content.document.querySelector(selectors.addressAutofillCheckbox) + .disabled, + true, + "Autofill addresses checkbox should be disabled" + ); + is( + content.document.querySelector(selectors.creditCardAutofillCheckbox) + .disabled, + true, + "Autofill credit cards checkbox should be disabled" + ); + }); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_remoteiframe.js b/browser/extensions/formautofill/test/browser/browser_remoteiframe.js new file mode 100644 index 0000000000..4dddd27a09 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_remoteiframe.js @@ -0,0 +1,127 @@ +"use strict"; + +const IFRAME_URL_PATH = BASE_URL + "autocomplete_iframe.html"; + +// Start by adding a few addresses to storage. +add_task(async function setup_storage() { + await SpecialPowers.pushPrefEnv({ + set: [ + [AUTOFILL_ADDRESSES_AVAILABLE_PREF, "on"], + [ENABLED_AUTOFILL_ADDRESSES_PREF, true], + [ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF, true], + ], + }); + await setStorage(TEST_ADDRESS_2, TEST_ADDRESS_4, TEST_ADDRESS_5); +}); + +// Verify that form fillin works in a remote iframe, and that changing +// a field updates storage. +add_task(async function test_iframe_autocomplete() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + IFRAME_URL_PATH, + true + ); + let browser = tab.linkedBrowser; + let iframeBC = browser.browsingContext.children[1]; + await openPopupOnSubframe(browser, iframeBC, "#street-address"); + + // Highlight the first item in the list. We want to verify + // that the warning text is correct to ensure that the preview is + // performed properly. + + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC); + await expectWarningText(browser, "Autofills address"); + + // Highlight and select the second item in the list + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC); + await expectWarningText(browser, "Also autofills organization, email"); + EventUtils.synthesizeKey("VK_RETURN", {}); + + let onLoaded = BrowserTestUtils.browserLoaded(browser, true); + await SpecialPowers.spawn(iframeBC, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + return ( + content.document.getElementById("street-address").value == + "32 Vassar Street MIT Room 32-G524" && + content.document.getElementById("country").value == "US" && + content.document.getElementById("organization").value == + "World Wide Web Consortium" + ); + }); + }); + + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(iframeBC, { + focusSelector: "#organization", + newValues: { + "#tel": "+16172535702", + }, + }); + await onPopupShown; + await onLoaded; + + let onUpdated = waitForStorageChangedEvents("update"); + await clickDoorhangerButton(MAIN_BUTTON); + await onUpdated; + + // Check that the tel number was updated properly. + let addresses = await getAddresses(); + is(addresses.length, 3, "Still 3 address in storage"); + is(addresses[1].tel, "+16172535702", "Verify the tel field"); + + // Fill in the details again and then clear the form from the dropdown. + await openPopupOnSubframe(browser, iframeBC, "#street-address"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC); + EventUtils.synthesizeKey("VK_RETURN", {}); + + await waitForAutofill(iframeBC, "#tel", "+16172535702"); + + // Open the dropdown and select the Clear Form item. + await openPopupOnSubframe(browser, iframeBC, "#street-address"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC); + EventUtils.synthesizeKey("VK_RETURN", {}); + + await SpecialPowers.spawn(iframeBC, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + return ( + content.document.getElementById("street-address").value == "" && + content.document.getElementById("country").value == "" && + content.document.getElementById("organization").value == "" + ); + }); + }); + + await BrowserTestUtils.removeTab(tab); +}); + +// Choose preferences from the autocomplete dropdown within an iframe. +add_task(async function test_iframe_autocomplete_preferences() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + IFRAME_URL_PATH, + true + ); + let browser = tab.linkedBrowser; + let iframeBC = browser.browsingContext.children[1]; + await openPopupOnSubframe(browser, iframeBC, "#organization"); + + await expectWarningText(browser, "Also autofills address, phone, email"); + + const prefTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + PRIVACY_PREF_URL + ); + + // Select the preferences item. + EventUtils.synthesizeKey("VK_DOWN", {}); + EventUtils.synthesizeKey("VK_DOWN", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + + info(`expecting tab: about:preferences#privacy opened`); + const prefTab = await prefTabPromise; + info(`expecting tab: about:preferences#privacy removed`); + BrowserTestUtils.removeTab(prefTab); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js b/browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js new file mode 100644 index 0000000000..05adf40935 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js @@ -0,0 +1,37 @@ +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +add_task(async function test_add_address() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let addresses = await getAddresses(); + + is(addresses.length, 0, "No address in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser: privateWin.gBrowser, url: FORM_URL }, + async function (privateBrowser) { + await focusUpdateSubmitForm(privateBrowser, { + focusSelector: "#organization", + newValues: { + "#organization": "Mozilla", + "#street-address": "331 E. Evelyn Avenue", + "#tel": "1-650-903-0800", + }, + }); + } + ); + + await ensureNoAddressSaved(); + + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/browser/extensions/formautofill/test/browser/browser_update_doorhanger.js b/browser/extensions/formautofill/test/browser/browser_update_doorhanger.js new file mode 100644 index 0000000000..73fd903de6 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_update_doorhanger.js @@ -0,0 +1,189 @@ +"use strict"; + +/** + * Note that this testcase is built based on the assumption that subtests are + * run in order. So if you're going to add a new subtest, please add it in the end. + */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.addresses.capture.enabled", true]], + }); +}); + +add_task(async function test_update_address() { + await setStorage(TEST_ADDRESS_1); + let addresses = await getAddresses(); + is(addresses.length, 1, "1 address in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + // Autofill address fields + await openPopupOn(browser, "form #organization"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill(browser, "#tel", addresses[0].tel); + + // Update address fields and submit + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#organization", + newValues: { + "#organization": "Mozilla", + }, + }); + + await onPopupShown; + + // Choose to update address by doorhanger + let onUpdated = waitForStorageChangedEvents("update"); + await clickDoorhangerButton(MAIN_BUTTON); + await onUpdated; + } + ); + + addresses = await getAddresses(); + is(addresses.length, 1, "Still 1 address in storage"); + is(addresses[0].organization, "Mozilla", "Verify the organization field"); +}); + +add_task(async function test_create_new_address() { + let addresses = await getAddresses(); + is(addresses.length, 1, "1 address in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + // Autofill address fields + await openPopupOn(browser, "form #tel"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill(browser, "#tel", addresses[0].tel); + + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#tel", + newValues: { + "#tel": "+1234567890", + }, + }); + + await onPopupShown; + + // Choose to add address by doorhanger + let onAdded = waitForStorageChangedEvents("add"); + await clickDoorhangerButton(SECONDARY_BUTTON); + await onAdded; + } + ); + + addresses = await getAddresses(); + is(addresses.length, 2, "2 addresses in storage"); + is(addresses[1].tel, "+1234567890", "Verify the tel field"); +}); + +add_task(async function test_create_new_address_merge() { + let addresses = await getAddresses(); + is(addresses.length, 2, "2 addresses in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + await openPopupOn(browser, "form #tel"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill(browser, "#tel", addresses[1].tel); + + // Choose the latest address and revert to the original phone number + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#tel", + newValues: { + "#tel": "+16172535702", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); + + addresses = await getAddresses(); + is(addresses.length, 2, "Still 2 addresses in storage"); +}); + +add_task(async function test_submit_untouched_fields() { + let addresses = await getAddresses(); + is(addresses.length, 2, "2 addresses in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: FORM_URL }, + async function (browser) { + await openPopupOn(browser, "form #organization"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill(browser, "#tel", addresses[0].tel); + + let onPopupShown = waitForPopupShown(); + await SpecialPowers.spawn(browser, [], async function () { + let form = content.document.getElementById("form"); + let tel = form.querySelector("#tel"); + tel.value = "12345"; // ".value" won't change the highlight status. + }); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#tel", + newValues: { + "#organization": "Organization", + }, + }); + await onPopupShown; + + let onUpdated = waitForStorageChangedEvents("update"); + await clickDoorhangerButton(MAIN_BUTTON); + await onUpdated; + } + ); + + addresses = await getAddresses(); + is(addresses.length, 2, "Still 2 addresses in storage"); + is(addresses[0].organization, "Organization", "organization should change"); + is(addresses[0].tel, "+16172535702", "tel should remain unchanged"); +}); + +add_task(async function test_submit_reduced_fields() { + let addresses = await getAddresses(); + is(addresses.length, 2, "2 addresses in storage"); + + let url = BASE_URL + "autocomplete_simple_basic.html"; + await BrowserTestUtils.withNewTab( + { gBrowser, url }, + async function (browser) { + await openPopupOn(browser, "form#simple input[name=tel]"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill(browser, "form #simple_tel", "6172535702"); + + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + formSelector: "form#simple", + focusSelector: "form #simple_tel", + newValues: { + "input[name=tel]": "123456789", + }, + }); + + await onPopupShown; + + let onUpdated = waitForStorageChangedEvents("update"); + await clickDoorhangerButton(MAIN_BUTTON); + await onUpdated; + } + ); + + addresses = await getAddresses(); + is(addresses.length, 2, "Still 2 addresses in storage"); + is(addresses[0].tel, "123456789", "tel should should be changed"); + is(addresses[0]["postal-code"], "02139", "postal code should be kept"); +}); 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: `
+ + + +
`, + idsToShowPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: "without @autocomplete - all fields in the same form", + document: `
+ + + +
`, + idsToShowPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: "@autocomplete - each field in its own form", + document: `
+
+
`, + idsToShowPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: + "without @autocomplete - each field in its own form (high-confidence cc-number & cc-name)", + document: `
+
+
`, + 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: `
+
+
`, + 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 in a form", + document: `
+ + +
+
+ + +
`, + idsToShowPopup: ["cc-number", "cc-name"], + }, + { + description: + "without @autocomplete - high-confidence cc-number/cc-name and another in a form", + document: `
+ + +
+
+ + +
`, + idsWithNoPopup: ["cc-number", "cc-name"], + }, + { + description: + "without @autocomplete - high-confidence cc-number/cc-name and another hidden in a form", + document: `
+ + +
+
+ + +
`, + 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 `); + } + + 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: ` +
+ + + +
+
+ + + +
`, + 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 */ diff --git a/browser/extensions/formautofill/test/browser/empty.html b/browser/extensions/formautofill/test/browser/empty.html new file mode 100644 index 0000000000..1ad28bb1f7 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/empty.html @@ -0,0 +1,8 @@ + + + +Empty file + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/test-setup.sh b/browser/extensions/formautofill/test/browser/fathom/test-setup.sh new file mode 100755 index 0000000000..1547c4a395 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/test-setup.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# This script download samples for testing + +clean=1 +cleanall=0 +sample_dir=autofill-repo-samples + +while [ $# -gt 0 ]; do + case "$1" in + -c) clean=1 ;; + -cc) cleanall=1 ;; + esac + shift +done + +# Check out source code +if ! [ -d "fathom-form-autofill" ]; then + echo "Get samples from repo..." + git clone https://github.com/mozilla-services/fathom-form-autofill +fi + +if ! [ -d "samples" ]; then + echo "Copy samples..." + mkdir $sample_dir + cp -r fathom-form-autofill/samples/testing $sample_dir + cp -r fathom-form-autofill/samples/training $sample_dir + cp -r fathom-form-autofill/samples/validation $sample_dir +else + echo "\`samples\` directory already exists" +fi + +if [ "$clean" = 1 ] || [ "$cleanall" = 1 ]; then + echo "Cleanup..." + rm -rf fathom-form-autofill +fi + +if [ "$cleanall" = 1 ]; then + rm -rf $sample_dir +fi diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg new file mode 100644 index 0000000000..179f3b5cce --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg @@ -0,0 +1,3 @@ + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg new file mode 100644 index 0000000000..619b82106d --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png new file mode 100644 index 0000000000..446ba13cec Binary files /dev/null and b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png differ diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif new file mode 100644 index 0000000000..b3aa80d843 Binary files /dev/null and b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif differ diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg new file mode 100644 index 0000000000..ed16723fa0 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg new file mode 100644 index 0000000000..21f7d71bf6 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg new file mode 100644 index 0000000000..44c5c294d8 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg @@ -0,0 +1 @@ + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg new file mode 100644 index 0000000000..05105ed133 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg @@ -0,0 +1,11 @@ + + + ASDA Logo - White + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin new file mode 100644 index 0000000000..5fa299eb94 Binary files /dev/null and b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin differ diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg new file mode 100644 index 0000000000..6f66ac60a2 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg @@ -0,0 +1 @@ +Powered by \ No newline at end of file diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg new file mode 100644 index 0000000000..01977c77b0 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg new file mode 100644 index 0000000000..bb024231ef --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg new file mode 100644 index 0000000000..634943eee9 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg new file mode 100644 index 0000000000..857064edc9 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg new file mode 100644 index 0000000000..0678ad42f7 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2 b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2 new file mode 100644 index 0000000000..52b6d6916a Binary files /dev/null and b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2 differ diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2 b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2 new file mode 100644 index 0000000000..e379beda77 Binary files /dev/null and b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2 differ diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2 b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2 new file mode 100644 index 0000000000..efa300c564 Binary files /dev/null and b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2 differ diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/sample.html b/browser/extensions/formautofill/test/browser/fathom/testing/sample.html new file mode 100644 index 0000000000..ddb0f5f2da --- /dev/null +++ b/browser/extensions/formautofill/test/browser/fathom/testing/sample.html @@ -0,0 +1,20 @@ +My Account | Account Settings – Asda Groceries
diff --git a/browser/extensions/formautofill/test/browser/focus-leak/browser.ini b/browser/extensions/formautofill/test/browser/focus-leak/browser.ini new file mode 100644 index 0000000000..fc4aca4a35 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/focus-leak/browser.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = + doc_iframe_typecontent_input_focus.xhtml + doc_iframe_typecontent_input_focus_frame.html + ../head.js + +# This test is used to detect a leak. +# Keep it isolated in a dedicated test folder to make sure the leak is cleaned +# up as a sideeffect of another test. +[browser_iframe_typecontent_input_focus.js] +skip-if = + apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs diff --git a/browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js b/browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js new file mode 100644 index 0000000000..407c2b5163 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL_ROOT = + "chrome://mochitests/content/browser/browser/extensions/formautofill/test/browser/focus-leak/"; + +const XUL_FRAME_URI = URL_ROOT + "doc_iframe_typecontent_input_focus.xhtml"; +const INNER_HTML_FRAME_URI = + URL_ROOT + "doc_iframe_typecontent_input_focus_frame.html"; + +/** + * Check that focusing an input in a frame with type=content embedded in a xul + * document does not leak. + */ +add_task(async function () { + const doc = gBrowser.ownerDocument; + const linkedBrowser = gBrowser.selectedTab.linkedBrowser; + const browserContainer = gBrowser.getBrowserContainer(linkedBrowser); + + info("Load the test page in a frame with type content"); + const frame = doc.createXULElement("iframe"); + frame.setAttribute("type", "content"); + browserContainer.appendChild(frame); + + info("Wait for the xul iframe to be loaded"); + const onXulFrameLoad = BrowserTestUtils.waitForEvent(frame, "load", true); + frame.setAttribute("src", XUL_FRAME_URI); + await onXulFrameLoad; + + const panelFrame = frame.contentDocument.querySelector("#html-iframe"); + + info("Wait for the html iframe to be loaded"); + const onFrameLoad = BrowserTestUtils.waitForEvent(panelFrame, "load", true); + panelFrame.setAttribute("src", INNER_HTML_FRAME_URI); + await onFrameLoad; + + info("Focus an input inside the iframe"); + const focusMeInput = panelFrame.contentDocument.querySelector(".focusme"); + const onFocus = BrowserTestUtils.waitForEvent(focusMeInput, "focus"); + await SimpleTest.promiseFocus(panelFrame); + focusMeInput.focus(); + await onFocus; + + // This assert is not really meaningful, the main purpose of the test is + // to check against leaks. + is( + focusMeInput, + panelFrame.contentDocument.activeElement, + "The .focusme input is the active element" + ); + + info("Remove the focused input"); + focusMeInput.remove(); +}); diff --git a/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml new file mode 100644 index 0000000000..ec3aaa2648 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml @@ -0,0 +1,7 @@ + + + + + + diff --git a/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html new file mode 100644 index 0000000000..00853d8eec --- /dev/null +++ b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js new file mode 100644 index 0000000000..2dd3d1451e --- /dev/null +++ b/browser/extensions/formautofill/test/browser/head.js @@ -0,0 +1,1094 @@ +"use strict"; + +const { OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" +); +const { OSKeyStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/OSKeyStoreTestUtils.sys.mjs" +); + +const { FormAutofillParent } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillParent.sys.mjs" +); + +const MANAGE_ADDRESSES_DIALOG_URL = + "chrome://formautofill/content/manageAddresses.xhtml"; +const MANAGE_CREDIT_CARDS_DIALOG_URL = + "chrome://formautofill/content/manageCreditCards.xhtml"; +const EDIT_ADDRESS_DIALOG_URL = + "chrome://formautofill/content/editAddress.xhtml"; +const EDIT_CREDIT_CARD_DIALOG_URL = + "chrome://formautofill/content/editCreditCard.xhtml"; +const PRIVACY_PREF_URL = "about:preferences#privacy"; + +const HTTP_TEST_PATH = "/browser/browser/extensions/formautofill/test/browser/"; +const BASE_URL = "http://mochi.test:8888" + HTTP_TEST_PATH; +const FORM_URL = BASE_URL + "autocomplete_basic.html"; +const ADDRESS_FORM_URL = + "https://example.org" + + HTTP_TEST_PATH + + "address/autocomplete_address_basic.html"; +const ADDRESS_FORM_WITHOUT_AUTOCOMPLETE_URL = + "https://example.org" + + HTTP_TEST_PATH + + "address/without_autocomplete_address_basic.html"; +const CREDITCARD_FORM_URL = + "https://example.org" + + HTTP_TEST_PATH + + "creditCard/autocomplete_creditcard_basic.html"; +const CREDITCARD_FORM_IFRAME_URL = + "https://example.org" + + HTTP_TEST_PATH + + "creditCard/autocomplete_creditcard_iframe.html"; +const CREDITCARD_FORM_COMBINED_EXPIRY_URL = + "https://example.org" + + HTTP_TEST_PATH + + "creditCard/autocomplete_creditcard_cc_exp_field.html"; +const CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL = + "https://example.org" + + HTTP_TEST_PATH + + "creditCard/without_autocomplete_creditcard_basic.html"; +const EMPTY_URL = "https://example.org" + HTTP_TEST_PATH + "empty.html"; + +const FTU_PREF = "extensions.formautofill.firstTimeUse"; +const ENABLED_AUTOFILL_ADDRESSES_PREF = + "extensions.formautofill.addresses.enabled"; +const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF = + "extensions.formautofill.addresses.capture.enabled"; +const AUTOFILL_ADDRESSES_AVAILABLE_PREF = + "extensions.formautofill.addresses.supported"; +const ENABLED_AUTOFILL_ADDRESSES_SUPPORTED_COUNTRIES_PREF = + "extensions.formautofill.addresses.supportedCountries"; +const AUTOFILL_CREDITCARDS_AVAILABLE_PREF = + "extensions.formautofill.creditCards.supported"; +const ENABLED_AUTOFILL_CREDITCARDS_PREF = + "extensions.formautofill.creditCards.enabled"; +const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries"; +const SYNC_USERNAME_PREF = "services.sync.username"; +const SYNC_ADDRESSES_PREF = "services.sync.engine.addresses"; +const SYNC_CREDITCARDS_PREF = "services.sync.engine.creditcards"; +const SYNC_CREDITCARDS_AVAILABLE_PREF = + "services.sync.engine.creditcards.available"; + +const TEST_ADDRESS_1 = { + "given-name": "John", + "additional-name": "R.", + "family-name": "Smith", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+16172535702", + email: "timbl@w3.org", +}; + +const TEST_ADDRESS_2 = { + "given-name": "Anonymouse", + "street-address": "Some Address", + country: "US", +}; + +const TEST_ADDRESS_3 = { + "given-name": "John", + "street-address": "Other Address", + "postal-code": "12345", +}; + +const TEST_ADDRESS_4 = { + "given-name": "Timothy", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + country: "US", + email: "timbl@w3.org", +}; + +// TODO: Number of field less than AUTOFILL_FIELDS_THRESHOLD +// need to confirm whether this is intentional +const TEST_ADDRESS_5 = { + tel: "+16172535702", +}; + +const TEST_ADDRESS_CA_1 = { + "given-name": "John", + "additional-name": "R.", + "family-name": "Smith", + organization: "Mozilla", + "street-address": "163 W Hastings\nSuite 209", + "address-level2": "Vancouver", + "address-level1": "BC", + "postal-code": "V6B 1H5", + country: "CA", + tel: "+17787851540", + email: "timbl@w3.org", +}; + +const TEST_ADDRESS_DE_1 = { + "given-name": "John", + "additional-name": "R.", + "family-name": "Smith", + organization: "Mozilla", + "street-address": + "Geb\u00E4ude 3, 4. Obergeschoss\nSchlesische Stra\u00DFe 27", + "address-level2": "Berlin", + "postal-code": "10997", + country: "DE", + tel: "+4930983333000", + email: "timbl@w3.org", +}; + +const TEST_ADDRESS_IE_1 = { + "given-name": "Bob", + "additional-name": "Z.", + "family-name": "Builder", + organization: "Best Co.", + "street-address": "123 Kilkenny St.", + "address-level3": "Some Townland", + "address-level2": "Dublin", + "address-level1": "Co. Dublin", + "postal-code": "A65 F4E2", + country: "IE", + tel: "+13534564947391", + email: "ie@example.com", +}; + +const TEST_CREDIT_CARD_1 = { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 4, + "cc-exp-year": new Date().getFullYear(), +}; + +const TEST_CREDIT_CARD_2 = { + "cc-name": "Timothy Berners-Lee", + "cc-number": "4929001587121045", + "cc-exp-month": 12, + "cc-exp-year": new Date().getFullYear() + 10, +}; + +const TEST_CREDIT_CARD_3 = { + "cc-number": "5103059495477870", + "cc-exp-month": 1, + "cc-exp-year": 2000, +}; + +const TEST_CREDIT_CARD_4 = { + "cc-number": "5105105105105100", +}; + +const TEST_CREDIT_CARD_5 = { + "cc-name": "Chris P. Bacon", + "cc-number": "4012888888881881", +}; + +const MAIN_BUTTON = "button"; +const SECONDARY_BUTTON = "secondaryButton"; +const MENU_BUTTON = "menubutton"; + +/** + * Collection of timeouts that are used to ensure something should not happen. + */ +const TIMEOUT_ENSURE_PROFILE_NOT_SAVED = 1000; +const TIMEOUT_ENSURE_CC_DIALOG_NOT_CLOSED = 500; +const TIMEOUT_ENSURE_AUTOCOMPLETE_NOT_SHOWN = 1000; + +async function ensureCreditCardDialogNotClosed(win) { + const unloadHandler = () => { + ok(false, "Credit card dialog shouldn't be closed"); + }; + win.addEventListener("unload", unloadHandler); + await new Promise(resolve => + setTimeout(resolve, TIMEOUT_ENSURE_CC_DIALOG_NOT_CLOSED) + ); + win.removeEventListener("unload", unloadHandler); +} + +function getDisplayedPopupItems( + browser, + selector = ".autocomplete-richlistitem" +) { + info("getDisplayedPopupItems"); + const { + autoCompletePopup: { richlistbox: itemsBox }, + } = browser; + const listItemElems = itemsBox.querySelectorAll(selector); + + return [...listItemElems].filter( + item => item.getAttribute("collapsed") != "true" + ); +} + +async function sleep(ms = 500) { + await new Promise(resolve => setTimeout(resolve, ms)); +} + +async function ensureNoAutocompletePopup(browser) { + await new Promise(resolve => + setTimeout(resolve, TIMEOUT_ENSURE_AUTOCOMPLETE_NOT_SHOWN) + ); + const items = getDisplayedPopupItems(browser); + ok(!items.length, "Should not found autocomplete items"); +} + +/** + * Wait for "formautofill-storage-changed" events + * + * @param {Array} eventTypes + * eventType must be one of the following: + * `add`, `update`, `remove`, `notifyUsed`, `removeAll`, `reconcile` + * + * @returns {Promise} resolves when all events are received + */ +async function waitForStorageChangedEvents(...eventTypes) { + return Promise.all( + eventTypes.map(type => + TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => { + return data == type; + } + ) + ) + ); +} + +/** + * Wait until the element found matches the expected autofill value + * + * @param {object} target + * The target in which to run the task. + * @param {string} selector + * A selector used to query the element. + * @param {string} value + * The expected autofilling value for the element + */ +async function waitForAutofill(target, selector, value) { + await SpecialPowers.spawn( + target, + [selector, value], + async function (selector, val) { + await ContentTaskUtils.waitForCondition(() => { + let element = content.document.querySelector(selector); + return element.value == val; + }, "Autofill never fills"); + } + ); +} + +/** + * Waits for the subDialog to be loaded + * + * @param {Window} win The window of the dialog + * @param {string} dialogUrl The url of the dialog that we are waiting for + * + * @returns {Promise} resolves when the sub dialog is loaded + */ +function waitForSubDialogLoad(win, dialogUrl) { + return new Promise((resolve, reject) => { + win.gSubDialog._dialogStack.addEventListener( + "dialogopen", + async function dialogopen(evt) { + let cwin = evt.detail.dialog._frame.contentWindow; + if (cwin.location != dialogUrl) { + return; + } + content.gSubDialog._dialogStack.removeEventListener( + "dialogopen", + dialogopen + ); + + resolve(cwin); + } + ); + }); +} + +/** + * Use this function when you want to update the value of elements in + * a form and then submit the form. This function makes sure the form + * is "identified" (`identifyAutofillFields` is called) before submitting + * the form. + * This is guaranteed by first focusing on an element in the form to trigger + * the 'FormAutofill:FieldsIdentified' message. + * + * @param {object} target + * The target in which to run the task. + * @param {object} args + * @param {string} args.focusSelector + * A selector used to query the element to be focused + * @param {string} args.formId + * The id of the form to be updated. This function uses "form" if + * this argument is not present + * @param {string} args.formSelector + * A selector used to query the form element + * @param {object} args.newValues + * Elements to be updated. Key is the element selector, value is the + * new value of the element. + * + * @param {boolean} submit + * Set to true to submit the form after the task is done, false otherwise. + */ +async function focusUpdateSubmitForm(target, args, submit = true) { + let fieldsIdentifiedPromiseResolver; + let fieldsIdentifiedObserver = { + fieldsIdentified() { + FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver); + fieldsIdentifiedPromiseResolver(); + }, + }; + + let fieldsIdentifiedPromise = new Promise(resolve => { + fieldsIdentifiedPromiseResolver = resolve; + FormAutofillParent.addMessageObserver(fieldsIdentifiedObserver); + }); + + let alreadyFocused = await SpecialPowers.spawn(target, [args], obj => { + let focused = false; + + let form; + if (obj.formSelector) { + form = content.document.querySelector(obj.formSelector); + } else { + form = content.document.getElementById(obj.formId ?? "form"); + } + let element = form.querySelector(obj.focusSelector); + if (element != content.document.activeElement) { + info(`focus on element (id=${element.id})`); + element.focus(); + } else { + focused = true; + } + + for (const [selector, value] of Object.entries(obj.newValues)) { + element = form.querySelector(selector); + if (content.HTMLInputElement.isInstance(element)) { + element.setUserInput(value); + } else { + element.value = value; + } + } + + return focused; + }); + + if (alreadyFocused) { + // If the element is already focused, assume the FieldsIdentified message + // was sent before. + fieldsIdentifiedPromiseResolver(); + } + + await fieldsIdentifiedPromise; + + if (submit) { + await SpecialPowers.spawn(target, [args], obj => { + let form; + if (obj.formSelector) { + form = content.document.querySelector(obj.formSelector); + } else { + form = content.document.getElementById(obj.formId ?? "form"); + } + info(`submit form (id=${form.id})`); + form.querySelector("input[type=submit]").click(); + }); + } +} + +async function focusAndWaitForFieldsIdentified(browserOrContext, selector) { + info("expecting the target input being focused and identified"); + /* eslint no-shadow: ["error", { "allow": ["selector", "previouslyFocused", "previouslyIdentified"] }] */ + + // If the input is previously focused, no more notifications will be + // sent as the notification goes along with focus event. + let fieldsIdentifiedPromiseResolver; + let fieldsIdentifiedObserver = { + fieldsIdentified() { + fieldsIdentifiedPromiseResolver(); + }, + }; + + let fieldsIdentifiedPromise = new Promise(resolve => { + fieldsIdentifiedPromiseResolver = resolve; + FormAutofillParent.addMessageObserver(fieldsIdentifiedObserver); + }); + + const { previouslyFocused, previouslyIdentified } = await SpecialPowers.spawn( + browserOrContext, + [selector], + async function (selector) { + const { FormLikeFactory } = ChromeUtils.importESModule( + "resource://gre/modules/FormLikeFactory.sys.mjs" + ); + const input = content.document.querySelector(selector); + const rootElement = FormLikeFactory.findRootForField(input); + const previouslyFocused = content.document.activeElement == input; + const previouslyIdentified = rootElement.hasAttribute( + "test-formautofill-identified" + ); + + input.focus(); + + return { previouslyFocused, previouslyIdentified }; + } + ); + + // Only wait for the fields identified notification if the + // focus was not previously assigned to the input. + if (previouslyFocused) { + fieldsIdentifiedPromiseResolver(); + } else { + info("!previouslyFocused"); + } + + // If a browsing context was supplied, focus its parent frame as well. + if ( + BrowsingContext.isInstance(browserOrContext) && + browserOrContext.parent != browserOrContext + ) { + await SpecialPowers.spawn( + browserOrContext.parent, + [browserOrContext], + async function (browsingContext) { + browsingContext.embedderElement.focus(); + } + ); + } + + if (previouslyIdentified) { + info("previouslyIdentified"); + FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver); + return; + } + + // Wait 500ms to ensure that "markAsAutofillField" is completely finished. + await fieldsIdentifiedPromise; + info("FieldsIdentified"); + FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver); + + await sleep(); + await SpecialPowers.spawn(browserOrContext, [], async function () { + const { FormLikeFactory } = ChromeUtils.importESModule( + "resource://gre/modules/FormLikeFactory.sys.mjs" + ); + FormLikeFactory.findRootForField( + content.document.activeElement + ).setAttribute("test-formautofill-identified", "true"); + }); +} + +/** + * Run the task and wait until the autocomplete popup is opened. + * + * @param {object} browser A xul:browser. + * @param {Function} taskFn Task that will trigger the autocomplete popup + */ +async function runAndWaitForAutocompletePopupOpen(browser, taskFn) { + info("runAndWaitForAutocompletePopupOpen"); + let popupShown = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "shown" + ); + + // Run the task will open the autocomplete popup + await taskFn(); + + await popupShown; + await BrowserTestUtils.waitForMutationCondition( + browser.autoCompletePopup.richlistbox, + { childList: true, subtree: true, attributes: true }, + () => { + const listItemElems = getDisplayedPopupItems(browser); + return ( + !![...listItemElems].length && + [...listItemElems].every(item => { + return ( + (item.getAttribute("originaltype") == "autofill-profile" || + item.getAttribute("originaltype") == "autofill-insecureWarning" || + item.getAttribute("originaltype") == "autofill-clear-button" || + item.getAttribute("originaltype") == "autofill-footer") && + item.hasAttribute("formautofillattached") + ); + }) + ); + } + ); +} + +async function waitForPopupEnabled(browser) { + const { + autoCompletePopup: { richlistbox: itemsBox }, + } = browser; + info("Wait for list elements to become enabled"); + await BrowserTestUtils.waitForMutationCondition( + itemsBox, + { subtree: true, attributes: true, attributeFilter: ["disabled"] }, + () => !itemsBox.querySelectorAll(".autocomplete-richlistitem")[0].disabled + ); +} + +// Wait for the popup state change notification to happen in a child process. +function waitPopupStateInChild(bc, messageName) { + return SpecialPowers.spawn(bc, [messageName], expectedMessage => { + return new Promise(resolve => { + const { AutoCompleteChild } = ChromeUtils.importESModule( + "resource://gre/actors/AutoCompleteChild.sys.mjs" + ); + + let listener = { + popupStateChanged: name => { + if (name != expectedMessage) { + info("Expected " + expectedMessage + " but received " + name); + return; + } + + AutoCompleteChild.removePopupStateListener(listener); + resolve(); + }, + }; + AutoCompleteChild.addPopupStateListener(listener); + }); + }); +} + +async function openPopupOn(browser, selector) { + let childNotifiedPromise = waitPopupStateInChild( + browser, + "FormAutoComplete:PopupOpened" + ); + await SimpleTest.promiseFocus(browser); + + await runAndWaitForAutocompletePopupOpen(browser, async () => { + await focusAndWaitForFieldsIdentified(browser, selector); + if (!selector.includes("cc-")) { + info(`openPopupOn: before VK_DOWN on ${selector}`); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + } + }); + + await childNotifiedPromise; +} + +async function openPopupOnSubframe(browser, frameBrowsingContext, selector) { + let childNotifiedPromise = waitPopupStateInChild( + frameBrowsingContext, + "FormAutoComplete:PopupOpened" + ); + + await SimpleTest.promiseFocus(browser); + + await runAndWaitForAutocompletePopupOpen(browser, async () => { + await focusAndWaitForFieldsIdentified(frameBrowsingContext, selector); + if (!selector.includes("cc-")) { + info(`openPopupOnSubframe: before VK_DOWN on ${selector}`); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, frameBrowsingContext); + } + }); + + await childNotifiedPromise; +} + +async function closePopup(browser) { + // Return if the popup isn't open. + if (!browser.autoCompletePopup.popupOpen) { + return; + } + + let childNotifiedPromise = waitPopupStateInChild( + browser, + "FormAutoComplete:PopupClosed" + ); + let popupClosePromise = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "hidden" + ); + + await SpecialPowers.spawn(browser, [], async function () { + content.document.activeElement.blur(); + }); + + await popupClosePromise; + await childNotifiedPromise; +} + +async function closePopupForSubframe(browser, frameBrowsingContext) { + let childNotifiedPromise = waitPopupStateInChild( + browser, + "FormAutoComplete:PopupClosed" + ); + + let popupClosePromise = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "hidden" + ); + + await SpecialPowers.spawn(frameBrowsingContext, [], async function () { + content.document.activeElement.blur(); + }); + + await popupClosePromise; + await childNotifiedPromise; +} + +function emulateMessageToBrowser(name, data) { + let actor = + gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( + "FormAutofill" + ); + return actor.receiveMessage({ name, data }); +} + +function getRecords(data) { + info(`expecting record retrievals: ${data.collectionName}`); + return emulateMessageToBrowser("FormAutofill:GetRecords", data); +} + +function getAddresses() { + return getRecords({ collectionName: "addresses" }); +} + +async function ensureNoAddressSaved() { + await new Promise(resolve => + setTimeout(resolve, TIMEOUT_ENSURE_PROFILE_NOT_SAVED) + ); + const addresses = await getAddresses(); + is(addresses.length, 0, "No address was saved"); +} + +function getCreditCards() { + return getRecords({ collectionName: "creditCards" }); +} + +async function saveAddress(address) { + info("expecting address saved"); + let observePromise = TestUtils.topicObserved("formautofill-storage-changed"); + await emulateMessageToBrowser("FormAutofill:SaveAddress", { address }); + await observePromise; +} + +async function saveCreditCard(creditcard) { + info("expecting credit card saved"); + let creditcardClone = Object.assign({}, creditcard); + let observePromise = TestUtils.topicObserved("formautofill-storage-changed"); + await emulateMessageToBrowser("FormAutofill:SaveCreditCard", { + creditcard: creditcardClone, + }); + await observePromise; +} + +async function removeAddresses(guids) { + info("expecting address removed"); + let observePromise = TestUtils.topicObserved("formautofill-storage-changed"); + await emulateMessageToBrowser("FormAutofill:RemoveAddresses", { guids }); + await observePromise; +} + +async function removeCreditCards(guids) { + info("expecting credit card removed"); + let observePromise = TestUtils.topicObserved("formautofill-storage-changed"); + await emulateMessageToBrowser("FormAutofill:RemoveCreditCards", { guids }); + await observePromise; +} + +function getNotification(index = 0) { + let notifications = PopupNotifications.panel.childNodes; + ok(!!notifications.length, "at least one notification displayed"); + ok(true, notifications.length + " notification(s)"); + return notifications[index]; +} + +function waitForPopupShown() { + return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); +} + +/** + * Clicks the popup notification button and wait for popup hidden. + * + * @param {string} button The button type in popup notification. + * @param {number} index The action's index in menu list. + */ +async function clickDoorhangerButton(button, index) { + let popuphidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + if (button == MAIN_BUTTON || button == SECONDARY_BUTTON) { + EventUtils.synthesizeMouseAtCenter(getNotification()[button], {}); + } else if (button == MENU_BUTTON) { + // Click the dropmarker arrow and wait for the menu to show up. + info("expecting notification menu button present"); + await BrowserTestUtils.waitForCondition(() => getNotification().menubutton); + await sleep(2000); // menubutton needs extra time for binding + let notification = getNotification(); + ok(notification.menubutton, "notification menupopup displayed"); + let dropdownPromise = BrowserTestUtils.waitForEvent( + notification.menupopup, + "popupshown" + ); + await EventUtils.synthesizeMouseAtCenter(notification.menubutton, {}); + info("expecting notification popup show up"); + await dropdownPromise; + + let actionMenuItem = notification.querySelectorAll("menuitem")[index]; + await EventUtils.synthesizeMouseAtCenter(actionMenuItem, {}); + } + info("expecting notification popup hidden"); + await popuphidden; +} + +function getDoorhangerCheckbox() { + return getNotification().checkbox; +} + +function getDoorhangerButton(button) { + return getNotification()[button]; +} + +/** + * Removes all addresses and credit cards from storage. + * + * **NOTE: If you add or update a record in a test, then you must wait for the + * respective storage event to fire before calling this function.** + * This is because this function doesn't guarantee that a record that + * is about to be added or update will also be removed, + * since the add or update is triggered by an asynchronous call. + * + * @see waitForStorageChangedEvents for more details about storage events to wait for + */ +async function removeAllRecords() { + let addresses = await getAddresses(); + if (addresses.length) { + await removeAddresses(addresses.map(address => address.guid)); + } + let creditCards = await getCreditCards(); + if (creditCards.length) { + await removeCreditCards(creditCards.map(cc => cc.guid)); + } +} + +async function waitForFocusAndFormReady(win) { + return Promise.all([ + new Promise(resolve => waitForFocus(resolve, win)), + BrowserTestUtils.waitForEvent(win, "FormReady"), + ]); +} + +// Verify that the warning in the autocomplete popup has the expected text. +async function expectWarningText(browser, expectedText) { + const { + autoCompletePopup: { richlistbox: itemsBox }, + } = browser; + let warningBox = itemsBox.querySelector( + ".autocomplete-richlistitem:last-child" + ); + + while (warningBox.collapsed) { + warningBox = warningBox.previousSibling; + } + warningBox = warningBox._warningTextBox; + + await BrowserTestUtils.waitForMutationCondition( + warningBox, + { childList: true, characterData: true }, + () => warningBox.textContent == expectedText + ); + ok(true, `Got expected warning text: ${expectedText}`); +} + +async function testDialog(url, testFn, arg = undefined) { + // Skip this step for test cards that lack an encrypted + // number since they will fail to decrypt. + if ( + url == EDIT_CREDIT_CARD_DIALOG_URL && + arg && + arg.record && + arg.record["cc-number-encrypted"] + ) { + arg.record = Object.assign({}, arg.record, { + "cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]), + }); + } + let win = window.openDialog(url, null, "width=600,height=600", arg); + await waitForFocusAndFormReady(win); + let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload"); + await testFn(win); + return unloadPromise; +} + +/** + * Initializes the test storage for a task. + * + * @param {...object} items Can either be credit card or address objects + */ +async function setStorage(...items) { + for (let item of items) { + if (item["cc-number"]) { + await saveCreditCard(item); + } else { + await saveAddress(item); + } + } +} + +function verifySectionAutofillResult(sections, expectedSectionsInfo) { + sections.forEach((section, index) => { + const expectedSection = expectedSectionsInfo[index]; + + const fieldDetails = section.fieldDetails; + const expectedFieldDetails = expectedSection.fields; + + info(`verify autofill section[${index}]`); + + fieldDetails.forEach((field, fieldIndex) => { + const expeceted = expectedFieldDetails[fieldIndex]; + + Assert.equal( + field.element.value, + expeceted.autofill, + `Autofilled value for element(id=${field.element.id}, field name=${field.fieldName}) should be equal` + ); + }); + }); +} + +function verifySectionFieldDetails(sections, expectedSectionsInfo) { + sections.forEach((section, index) => { + const expectedSection = expectedSectionsInfo[index]; + + const fieldDetails = section.fieldDetails; + const expectedFieldDetails = expectedSection.fields; + + info(`section[${index}] ${expectedSection.description ?? ""}:`); + info(`FieldName Prediction Results: ${fieldDetails.map(i => i.fieldName)}`); + info( + `FieldName Expected Results: ${expectedFieldDetails.map( + detail => detail.fieldName + )}` + ); + Assert.equal( + fieldDetails.length, + expectedFieldDetails.length, + `Expected field count.` + ); + + fieldDetails.forEach((field, fieldIndex) => { + const expectedFieldDetail = expectedFieldDetails[fieldIndex]; + + const expected = { + ...{ + reason: "autocomplete", + section: "", + contactType: "", + addressType: "", + }, + ...expectedSection.default, + ...expectedFieldDetail, + }; + + const keys = new Set([...Object.keys(field), ...Object.keys(expected)]); + ["autofill", "elementWeakRef", "confidence", "part"].forEach(k => + keys.delete(k) + ); + + for (const key of keys) { + const expectedValue = expected[key]; + const actualValue = field[key]; + Assert.equal( + actualValue, + expectedValue, + `${key} should be equal, expect ${expectedValue}, got ${actualValue}` + ); + } + }); + + Assert.equal( + section.isValidSection(), + !expectedSection.invalid, + `Should be an ${expectedSection.invalid ? "invalid" : "valid"} section` + ); + }); +} +/** + * Runs heuristics test for form autofill on given patterns. + * + * @param {Array} patterns - An array of test patterns to run the heuristics test on. + * @param {string} pattern.description - Description of this heuristic test + * @param {string} pattern.fixurePath - The path of the test document + * @param {string} pattern.fixureData - Test document by string. Use either fixurePath or fixtureData. + * @param {object} pattern.profile - The profile to autofill. This is required only when running autofill test + * @param {Array} pattern.expectedResult - The expected result of this heuristic test. See below for detailed explanation + * + * @param {string} [fixturePathPrefix=""] - The prefix to the path of fixture files. + * @param {object} [options={ testAutofill: false }] - An options object containing additional configuration for running the test. + * @param {boolean} [options.testAutofill=false] - A boolean indicating whether to run the test for autofill or not. + * @returns {Promise} A promise that resolves when all the tests are completed. + * + * The `patterns.expectedResult` array contains test data for different address or credit card sections. + * Each section in the array is represented by an object and can include the following properties: + * - description (optional): A string describing the section, primarily used for debugging purposes. + * - default (optional): An object that sets the default values for all the fields within this section. + * The default object contains the same keys as the individual field objects. + * - fields: An array of field details (class FieldDetails) within the section. + * + * Each field object can have the following keys: + * - fieldName: The name of the field (e.g., "street-name", "cc-name" or "cc-number"). + * - reason: The reason for the field value (e.g., "autocomplete", "regex-heuristic" or "fathom"). + * - section: The section to which the field belongs (e.g., "billing", "shipping"). + * - part: The part of the field. + * - contactType: The contact type of the field. + * - addressType: The address type of the field. + * - autofill: Set the expected autofill value when running autofill test + * + * For more information on the field object properties, refer to the FieldDetails class. + * + * Example test data: + * add_heuristic_tests( + * [{ + * description: "first test pattern", + * fixuturePath: "autocomplete_off.html", + * profile: {organization: "Mozilla", country: "US", tel: "123"}, + * expectedResult: [ + * { + * description: "First section" + * fields: [ + * { fieldName: "organization", reason: "autocomplete", autofill: "Mozilla" }, + * { fieldName: "country", reason: "regex-heuristic", autofill: "US" }, + * { fieldName: "tel", reason: "regex-heuristic", autofill: "123" }, + * ] + * }, + * { + * default: { + * reason: "regex-heuristic", + * section: "billing", + * }, + * fields: [ + * { fieldName: "cc-number", reason: "fathom" }, + * { fieldName: "cc-nane" }, + * { fieldName: "cc-exp" }, + * ], + * }], + * }, + * { + * // second test pattern // + * } + * ], + * "/fixturepath", + * {testAutofill: true} // test options + * ) + */ + +async function add_heuristic_tests( + patterns, + fixturePathPrefix = "", + options = { testAutofill: false } +) { + async function runTest(testPattern) { + const TEST_URL = testPattern.fixtureData + ? `data:text/html,${testPattern.fixtureData}` + : `${BASE_URL}../${fixturePathPrefix}${testPattern.fixturePath}`; + + if (testPattern.fixtureData) { + info(`Starting test with fixture data`); + } else { + info(`Starting test fixture: ${testPattern.fixturePath ?? ""}`); + } + + if (testPattern.description) { + info(`Test "${testPattern.description}"`); + } + + if (testPattern.prefs) { + await SpecialPowers.pushPrefEnv({ + set: testPattern.prefs, + }); + } + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn( + browser, + [ + { + testPattern, + verifySection: verifySectionFieldDetails.toString(), + verifyAutofill: options.testAutofill + ? verifySectionAutofillResult.toString() + : null, + }, + ], + async obj => { + const { FormLikeFactory } = ChromeUtils.importESModule( + "resource://gre/modules/FormLikeFactory.sys.mjs" + ); + const { FormAutofillHandler } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHandler.sys.mjs" + ); + + const elements = Array.from( + content.document.querySelectorAll("input, select") + ); + + // Bug 1834768. We should simulate user behavior instead of + // using internal APIs. + const forms = elements.reduce((acc, element) => { + const formLike = FormLikeFactory.createFromField(element); + if (!acc.some(form => form.rootElement === formLike.rootElement)) { + acc.push(formLike); + } + return acc; + }, []); + + const sections = forms.flatMap(form => { + const handler = new FormAutofillHandler(form); + handler.collectFormFields(false /* ignoreInvalid */); + return handler.sections; + }); + + Assert.equal( + sections.length, + obj.testPattern.expectedResult.length, + "Expected section count." + ); + + // eslint-disable-next-line no-eval + let verify = eval(`(() => {return (${obj.verifySection});})();`); + verify(sections, obj.testPattern.expectedResult); + + if (obj.verifyAutofill) { + for (const section of sections) { + section.focusedInput = section.fieldDetails[0].element; + await section.autofillFields( + section.getAdaptedProfiles([obj.testPattern.profile])[0] + ); + } + + // eslint-disable-next-line no-eval + verify = eval(`(() => {return (${obj.verifyAutofill});})();`); + verify(sections, obj.testPattern.expectedResult); + } + } + ); + }); + + if (testPattern.prefs) { + await SpecialPowers.popPrefEnv(); + } + } + + patterns.forEach(testPattern => { + add_task(() => runTest(testPattern)); + }); +} + +async function add_autofill_heuristic_tests(patterns, fixturePathPrefix = "") { + add_heuristic_tests(patterns, fixturePathPrefix, { testAutofill: true }); +} + +add_setup(function () { + OSKeyStoreTestUtils.setup(); +}); + +registerCleanupFunction(async () => { + await removeAllRecords(); + await OSKeyStoreTestUtils.cleanup(); +}); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser.ini b/browser/extensions/formautofill/test/browser/heuristics/browser.ini new file mode 100644 index 0000000000..cc93d6beea --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +support-files = + ../head.js + ../../fixtures/** + +[browser_autocomplete_off_on_form.js] +[browser_autocomplete_off_on_inputs.js] +[browser_basic.js] +[browser_cc_exp.js] +[browser_de_fields.js] +[browser_fr_fields.js] +[browser_ignore_invisible_fields.js] +[browser_multiple_section.js] +[browser_parseAddressFields.js] +[browser_section_validation_address.js] +[browser_sections_by_name.js] diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js new file mode 100644 index 0000000000..f93752bd1e --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global add_heuristic_tests */ + +// Ensures that fields are identified correctly even when the containing form +// has its autocomplete attribute set to off. +add_heuristic_tests( + [ + { + fixturePath: "autocomplete_off_on_form.html", + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-line3" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "address-line1" }, + { fieldName: "address-level2" }, + { fieldName: "address-line2" }, + { fieldName: "organization" }, + { fieldName: "address-line3" }, + ], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js new file mode 100644 index 0000000000..13c8d82dbb --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global add_heuristic_tests */ + +// Ensures that fields are identified correctly even when the inputs +// have their autocomplete attribute set to off. +add_heuristic_tests( + [ + { + fixturePath: "autocomplete_off_on_inputs.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-line3" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "cc-number", reason: "fathom" }, + { fieldName: "cc-name", reason: "fathom" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "address-line1" }, + { fieldName: "address-level2" }, + { fieldName: "address-line2" }, + { fieldName: "organization" }, + { fieldName: "address-line3" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "address-line1", reason: "regex-heuristic" }, + { fieldName: "address-line2", reason: "regex-heuristic" }, + { fieldName: "address-line3", reason: "regex-heuristic" }, + { fieldName: "address-level2", reason: "regex-heuristic" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code", reason: "regex-heuristic" }, + { fieldName: "country", reason: "regex-heuristic" }, + { fieldName: "tel" }, + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-number", reason: "fathom" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp-month", reason: "regex-heuristic" }, + { fieldName: "cc-exp-year", reason: "regex-heuristic" }, + ], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_basic.js b/browser/extensions/formautofill/test/browser/heuristics/browser_basic.js new file mode 100644 index 0000000000..fda375a146 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_basic.js @@ -0,0 +1,69 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "autocomplete_basic.html", + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "organization" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-line3" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "address-line1" }, + { fieldName: "address-level2" }, + { fieldName: "address-line2" }, + { fieldName: "organization" }, + { fieldName: "address-line3" }, + ], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js b/browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js new file mode 100644 index 0000000000..8eb774b65c --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js @@ -0,0 +1,56 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "heuristics_cc_exp.html", + expectedResult: [ + { + description: "form1", + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-name" }, + { fieldName: "cc-number" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + }, + { + description: "form2", + default: { + reason: "autocomplete", + }, + fields: [{ fieldName: "cc-number" }, { fieldName: "cc-exp" }], + }, + { + description: "form3", + fields: [ + { fieldName: "cc-number", reason: "autocomplete" }, + { fieldName: "cc-exp", reason: "regex-heuristic" }, + ], + }, + { + description: "form4", + fields: [ + { fieldName: "cc-number", reason: "autocomplete" }, + { fieldName: "cc-exp-month", reason: "regex-heuristic" }, + { fieldName: "cc-exp-year", reason: "regex-heuristic" }, + ], + }, + { + description: "form5", + invalid: true, + fields: [ + { fieldName: "cc-exp-month", reason: "regex-heuristic" }, + { fieldName: "cc-exp-year", reason: "regex-heuristic" }, + ], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js new file mode 100644 index 0000000000..0d47cb6935 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js @@ -0,0 +1,32 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "heuristics_de_fields.html", + expectedResult: [ + { + fields: [ + { fieldName: "cc-name", reason: "fathom" }, + { fieldName: "cc-type", reason: "regex-heuristic" }, + { fieldName: "cc-number", reason: "fathom" }, + { fieldName: "cc-exp-month", reason: "regex-heuristic" }, + { fieldName: "cc-exp-year", reason: "regex-heuristic" }, + ], + }, + { + fields: [ + { fieldName: "cc-name", reason: "fathom" }, + { fieldName: "cc-type", reason: "regex-heuristic" }, + { fieldName: "cc-number", reason: "fathom" }, + { fieldName: "cc-exp-month", reason: "regex-heuristic" }, + { fieldName: "cc-exp-year", reason: "regex-heuristic" }, + ], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js new file mode 100644 index 0000000000..a2c8add393 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "heuristics_fr_fields.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "cc-number", reason: "fathom" }, + { fieldName: "cc-exp" }, + { fieldName: "cc-name" }, + ], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_ignore_invisible_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_ignore_invisible_fields.js new file mode 100644 index 0000000000..d22b1d5031 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_ignore_invisible_fields.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests([ + { + description: "all fields are visible", + fixtureData: ` + + +
+ + + + + + +
+ +
+
+ + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "name" }, + { fieldName: "tel" }, + { fieldName: "email" }, + { fieldName: "country" }, + { fieldName: "postal-code" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + ], + }, + ], + }, + { + description: "some fields are invisible because of css style", + fixtureData: ` + + +
+ + + + + + +
+ +
+
+ + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "name" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + ], + }, + { + // hidden and style="display:none" are always considered regardless what visibility check we use + description: + "invisible fields are identified because number of elemenent in the form exceed the threshold", + prefs: [["extensions.formautofill.heuristics.visibilityCheckThreshold", 1]], + fixtureData: ` + + +
+ + + + + + +
+ +
+
+ + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "name" }, + { fieldName: "tel" }, + { fieldName: "email" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js b/browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js new file mode 100644 index 0000000000..078f2240db --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js @@ -0,0 +1,118 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "multiple_section.html", + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "shipping", + }, + fields: [ + { fieldName: "name", addressType: "" }, + { fieldName: "organization", addressType: "" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + section: "section-my", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel", section: "", contactType: "work" }, + { fieldName: "email", section: "", contactType: "work" }, + ], + }, + { + invalid: true, + default: { + reason: "autocomplete", + }, + fields: [ + // Even the `contactType` of these two fields are different with the + // above two, we still consider they are identical until supporting + // multiple phone number and email in one profile. + { fieldName: "tel", contactType: "home" }, + { fieldName: "email", contactType: "home" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "name" }, + { fieldName: "organization" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "tel", contactType: "work" }, + { fieldName: "email", contactType: "work" }, + ], + }, + { + invalid: true, + default: { + reason: "autocomplete", + contactType: "home", + }, + fields: [{ fieldName: "tel" }, { fieldName: "email" }], + }, + ], + }, + ], + "fixtures/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_parseAddressFields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_parseAddressFields.js new file mode 100644 index 0000000000..ffe739d417 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_parseAddressFields.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests([ + { + // This bug happens only when the last element is address-lineX and + // the field is identified by regular expressions in `HeuristicsRegExp` but is not + // identified by regular expressions defined in `_parseAddressFields` + fixtureData: ` + + +
+

+

+

+
+ + `, + expectedResult: [ + { + description: + "Address Line1 in the last element and is not updated in _parsedAddressFields", + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "address-line1" }, + ], + }, + ], + }, + { + fixtureData: ` + + +
+

+

+

+

+
+ + `, + expectedResult: [ + { + description: + "Address Line2 in the last element and is not updated in _parsedAddressFields", + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "country" }, + { fieldName: "tel" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + ], + }, + ], + }, + { + // Bug 1833613 + description: + "street-address field is treated as address-line1 when address-line2 is present while adddress-line1 is not", + fixtureData: ` + + +
+ + + +
+ + `, + expectedResult: [ + { + fields: [ + { fieldName: "address-line1", reason: "regexp-heuristic" }, + { fieldName: "address-line2", reason: "autocomplete" }, + { fieldName: "email", reason: "autocomplete" }, + ], + }, + ], + }, + { + // Bug 1833613 + description: + "street-address field should not be treated as address-line1 when address-line2 is not present", + fixtureData: ` + + +
+ + + +
+ + `, + expectedResult: [ + { + fields: [ + { fieldName: "street-address", reason: "autocomplete" }, + { fieldName: "address-line3", reason: "autocomplete" }, + { fieldName: "email", reason: "autocomplete" }, + ], + }, + ], + }, + { + // Bug 1833613 + description: + "street-address field should not be treated as address-line1 when address-line1 is present", + fixtureData: ` + + +
+ + + +
+ + `, + expectedResult: [ + { + fields: [ + { fieldName: "street-address", reason: "autocomplete" }, + { fieldName: "address-line1", reason: "autocomplete" }, + { fieldName: "email", reason: "autocomplete" }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js b/browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js new file mode 100644 index 0000000000..2e9cf42ab0 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests([ + { + description: `An address section is valid when it only contains more than three fields`, + fixtureData: ` + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "email" }, + ], + }, + ], + }, + { + description: `An address section is invalid when it contains less than threee fields`, + fixtureData: ` + + + + + + + `, + expectedResult: [ + { + description: "A section with two fields", + invalid: true, + fields: [ + { fieldName: "postal-code", reason: "autocomplete" }, + { fieldName: "email", reason: "autocomplete" }, + ], + }, + { + description: "A section with one field", + invalid: true, + fields: [{ fieldName: "postal-code", reason: "autocomplete" }], + }, + ], + }, + { + description: `Address section validation only counts the number of different address field name in the section`, + fixtureData: ` + + + + + + `, + expectedResult: [ + { + description: + "A section with three fields but has duplicated email fields", + invalid: true, + fields: [ + { fieldName: "postal-code", reason: "autocomplete" }, + { fieldName: "email", reason: "autocomplete" }, + { fieldName: "email", reason: "autocomplete" }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js b/browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js new file mode 100644 index 0000000000..c6c8ea5759 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js @@ -0,0 +1,318 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global add_heuristic_tests */ + +"use strict"; + +// The following are included in this test +// - One named billing section +// - One named billing section and one named shipping section +// - One named billing section and one section without name +// - Fields without section name are merged to a section with section name +// - Two sections without name + +add_heuristic_tests([ + { + description: `One named billing section`, + fixtureData: ` + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + ], + }, + { + description: `One billing section and one shipping section`, + fixtureData: ` + + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + addressType: "shipping", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + ], + }, + { + description: `One billing section, one shipping section, and then billing section`, + fixtureData: ` + + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + addressType: "shipping", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + ], + }, + { + description: `one section without a name and one billing section`, + fixtureData: ` + + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + ], + }, + { + description: `One billing section and one section without a name`, + fixtureData: ` + + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + ], + }, + { + description: `Fields without section name are merged (test both before and after the section with a name)`, + fixtureData: ` + + + + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "name", addressType: "" }, + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + addressType: "shipping", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "name", addressType: "" }, + ], + }, + ], + }, + { + description: `Fields without section name are merged, but do not merge if the field already exists`, + fixtureData: ` + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "name", addressType: "" }, + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + invalid: true, + fields: [{ fieldName: "name", reason: "autocomplete" }], + }, + ], + }, + { + description: `Fields without section name are merged (multi-fields)`, + fixtureData: ` + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + addressType: "billing", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + { fieldName: "email", addressType: "" }, + { fieldName: "email", addressType: "" }, + ], + }, + ], + }, + { + description: `Two sections without name`, + fixtureData: ` + + + + + + + + + `, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + { fieldName: "country" }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser.ini b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser.ini new file mode 100644 index 0000000000..6519ce9ae8 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +support-files = + ../../head.js + ../../../fixtures/** + +[browser_BestBuy.js] +[browser_CDW.js] +[browser_CostCo.js] +[browser_DirectAsda.js] +[browser_Ebay.js] +[browser_GlobalDirectAsda.js] +[browser_HomeDepot.js] +[browser_Lufthansa.js] +[browser_Lush.js] +[browser_Macys.js] +[browser_NewEgg.js] +[browser_OfficeDepot.js] +[browser_QVC.js] +[browser_Sears.js] +[browser_Staples.js] +[browser_Walmart.js] diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js new file mode 100644 index 0000000000..e91c19de45 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js @@ -0,0 +1,82 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "Checkout_ShippingAddress.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, // sign-up + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + { fieldName: "tel", reason: "regex-heuristic" }, + ], + }, + ], + }, + { + fixturePath: "Checkout_Payment.html", + expectedResult: [ + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + { fieldName: "tel", reason: "regex-heuristic" }, + ], + }, + ], + }, + { + fixturePath: "SignIn.html", + expectedResult: [ + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, // sign-in + ], + }, + ], + }, + ], + "fixtures/third_party/BestBuy/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js new file mode 100644 index 0000000000..feae321cb2 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js @@ -0,0 +1,71 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "Checkout_ShippingInfo.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "organization" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + { fieldName: "postal-code" }, // EXt + { fieldName: "email" }, + { fieldName: "tel" }, + { fieldName: "tel-extension" }, + ], + }, + ], + }, + { + fixturePath: "Checkout_BillingPaymentInfo.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "organization" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + { fieldName: "postal-code" }, // Ext + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "cc-type" }, // ac-off + { fieldName: "cc-number", reason: "fathom" }, // ac-off + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + // {fieldName: "cc-csc"}, + ], + }, + ], + }, + { + fixturePath: "Checkout_Logon.html", + expectedResult: [ + ], + }, + ], + "fixtures/third_party/CDW/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js new file mode 100644 index 0000000000..c83973b3c9 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js @@ -0,0 +1,170 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "ShippingAddress.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "additional-name" }, // middle-name initial + { fieldName: "family-name" }, + { fieldName: "organization" }, + { fieldName: "country" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "additional-name" }, // middle-name initial + { fieldName: "family-name" }, + { fieldName: "organization" }, + { fieldName: "country" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + ], + }, + { + fixturePath: "Payment.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "cc-type" }, // ac-off + { fieldName: "cc-number", reason: "fathom" }, // ac-off + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + // { fieldName: "cc-csc"}, // ac-off + { fieldName: "cc-name", reason: "fathom" }, // ac-off + ], + }, + { + invalid: true, // confidence is not high enough + fields: [ + { fieldName: "cc-number", reason: "fathom" }, // ac-off + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "additional-name" }, // middle-name initial + { fieldName: "family-name" }, + { fieldName: "organization" }, + { fieldName: "country" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-level2" }, // city + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "additional-name" }, // middle-name initial + { fieldName: "family-name" }, + { fieldName: "organization" }, + { fieldName: "country" }, + { fieldName: "address-line1" }, + { fieldName: "address-line2" }, + { fieldName: "address-level2" }, + { fieldName: "address-level1" }, // state + { fieldName: "postal-code" }, + { fieldName: "tel" }, + { fieldName: "email" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + ], + }, + { + fixturePath: "SignIn.html", + expectedResult: [ + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + { + invalid: true, + fields: [ + // Forgot password + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + // {fieldName: "password"}, + ], + }, + { + invalid: true, + fields: [ + // Sign up + { fieldName: "email", reason: "regex-heuristic" }, + ], + }, + { + invalid: true, + fields: [ + { fieldName: "email", reason: "regex-heuristic" }, + ], + } + ], + }, + ], + "fixtures/third_party/CostCo/" +); diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js new file mode 100644 index 0000000000..6cb7979173 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js @@ -0,0 +1,25 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "Payment.html", + expectedResult: [ + { + default: { + reason: "fathom", + }, + fields: [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp-month", reason: "regex-heuristic" }, + { fieldName: "cc-exp-year", reason: "regex-heuristic" }, + ], + }, + ], + }, + ], + "fixtures/third_party/DirectAsda/" +) diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js new file mode 100644 index 0000000000..db6047718f --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js @@ -0,0 +1,25 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "Checkout_Payment_FR.html", + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-number" }, + { fieldName: "cc-exp" }, + { fieldName: "cc-given-name" }, + { fieldName: "cc-family-name" }, + ], + }, + ], + }, + ], + "fixtures/third_party/Ebay/" +) diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js new file mode 100644 index 0000000000..7b82009032 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js @@ -0,0 +1,24 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "Payment.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "cc-number", reason: "fathom" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + }, + ], + }, + ], + "fixtures/third_party/GlobalDirectAsda/" +) diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js new file mode 100644 index 0000000000..8aac002b2d --- /dev/null +++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js @@ -0,0 +1,77 @@ +/* global add_heuristic_tests */ + +"use strict"; + +add_heuristic_tests( + [ + { + fixturePath: "Checkout_ShippingPayment.html", + expectedResult: [ + { + default: { + reason: "regex-heuristic", + }, + fields: [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "email" }, + { fieldName: "tel" }, + { fieldName: "street-address" }, + { fieldName: "postal-code" }, + ], + }, + { + invalid: true, + fields: [ + { + fieldName: "street-address", + reason: "autocomplete", + addressType: "billing", + }, + ], + }, + // THis should be fixed by visibility check + { + default: { + reason: "autocomplete", + }, + fields: [ + // FIXME: bug 1392944 - the uncommented cc-exp-month and cc-exp-year are + // both invisible elements, and the following two

+

+

+

+

+

+

+

+

+

+

+ + +

+ + + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html b/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html new file mode 100644 index 0000000000..5007e9e5bd --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html @@ -0,0 +1,52 @@ + + + + + Form Autofill Demo Page + + +

Form Autofill Demo Page

+
+

+

+

+

+

+

+

+

+

+

+
+ +
+

+

+

+

+

+

+

+

+

+

+
+

+

+

+

+
+

+

+
+ +
+

+

+

+

+

+
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html new file mode 100644 index 0000000000..1c03f2434d --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html @@ -0,0 +1,29 @@ + + + + + Form Autofill Credit Card Demo Page + + +

Form Autofill Credit Card Demo Page

+
+

+

+

+

+

+

+

+ + +

+
+ + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_cc_exp_field.html b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_cc_exp_field.html new file mode 100644 index 0000000000..24e93b73a2 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_cc_exp_field.html @@ -0,0 +1,28 @@ + + + + + Form Autofill Credit Card Demo Page + + +

Form Autofill Credit Card Demo Page

+
+

+

+

+

+

+

+ + +

+
+ + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_iframe.html b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_iframe.html new file mode 100644 index 0000000000..506deb396b --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_iframe.html @@ -0,0 +1,12 @@ + + + + + Form Autofill Credit Card With Remote IFrame Demo Page + + + + + + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_iframe.html b/browser/extensions/formautofill/test/fixtures/autocomplete_iframe.html new file mode 100644 index 0000000000..84fcf54e67 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_iframe.html @@ -0,0 +1,13 @@ + + + + + Form Autofill With Remote IFrame Demo Page + + + + + + + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_off_on_form.html b/browser/extensions/formautofill/test/fixtures/autocomplete_off_on_form.html new file mode 100644 index 0000000000..c7df012b36 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_off_on_form.html @@ -0,0 +1,52 @@ + + + + + Form Autofill Demo Page with autocomplete set to off on form elements + + +

Form Autofill Demo Page with autocomplete set to off on form elements

+
+

+

+

+

+

+

+

+

+

+

+
+ +
+

+

+

+

+

+

+

+

+

+

+
+

+

+

+

+
+

+

+
+ +
+

+

+

+

+

+
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_off_on_inputs.html b/browser/extensions/formautofill/test/fixtures/autocomplete_off_on_inputs.html new file mode 100644 index 0000000000..293240b9cc --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_off_on_inputs.html @@ -0,0 +1,77 @@ + + + + + Form Autofill Demo Page with autocomplete set to off on inputs within form elements + + +

Form Autofill Demo Page with autocomplete set to off on inputs within form elements

+
+

+

+

+

+

+

+

+

+

+

+
+ +
+

+

+

+

+

+

+

+

+

+

+
+

+

+

+

+
+

+

+
+ +
+

+

+

+

+

+
+ +
+ +

+

+

+

+

+

+

+

+

+

+
+

+

+

+

+
+

+

+
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_simple_basic.html b/browser/extensions/formautofill/test/fixtures/autocomplete_simple_basic.html new file mode 100644 index 0000000000..9e78e6392f --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/autocomplete_simple_basic.html @@ -0,0 +1,19 @@ + + + + + Form Autofill Demo Page for Simplified Form Case + + +

Form Autofill Demo Page for Simplified Form Case

+ +
+

+

+

+

+

+
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/heuristics_cc_exp.html b/browser/extensions/formautofill/test/fixtures/heuristics_cc_exp.html new file mode 100644 index 0000000000..e14f1520a6 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/heuristics_cc_exp.html @@ -0,0 +1,73 @@ + + + + + Heuristics cc-exp field test page + + +

Heuristics cc-exp field test page

+ +
+

+

+

+

+

+
+ +
+

+

+
+ +
+

+

+
+ +
+

+

+ +

+

+ +

+
+ +
+ + + +
+ + diff --git a/browser/extensions/formautofill/test/fixtures/heuristics_de_fields.html b/browser/extensions/formautofill/test/fixtures/heuristics_de_fields.html new file mode 100644 index 0000000000..1d34d6126a --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/heuristics_de_fields.html @@ -0,0 +1,122 @@ + + + + + Heuristics de-DE fields test page + + +

Heuristics de-DE fields test page

+
+
+
Karteninhaber
+ +
+
+
Kartentyp
+ +
+
+
Kartennummer
+ +
+
+
gültig bis
+ + +
+
+
Prüfnummer
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
gültig bis
+ + +
+
+
Prüfnummer
+ +
+
+ + diff --git a/browser/extensions/formautofill/test/fixtures/heuristics_fr_fields.html b/browser/extensions/formautofill/test/fixtures/heuristics_fr_fields.html new file mode 100644 index 0000000000..922741c853 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/heuristics_fr_fields.html @@ -0,0 +1,34 @@ + + + + + + + Heuristics fr-FR fields test page + + + +

Heuristics fr-FR fields test page

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/multiple_section.html b/browser/extensions/formautofill/test/fixtures/multiple_section.html new file mode 100644 index 0000000000..451a622521 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/multiple_section.html @@ -0,0 +1,84 @@ + + + + + Form Autofill Demo Page + + +

Form Autofill Demo Page

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+

+ + +

+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+

+ + +

+
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_Payment.html new file mode 100644 index 0000000000..41bece3ef2 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_Payment.html @@ -0,0 +1,283 @@ + + + + Checkout – Best Buy + + +
+ + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
    +
  • + +
  • +
+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_ShippingAddress.html b/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_ShippingAddress.html new file mode 100644 index 0000000000..f8b88d0778 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_ShippingAddress.html @@ -0,0 +1,326 @@ + + + + Checkout – Best Buy + + +
+ + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
    +
  • + +
  • +
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/SignIn.html b/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/SignIn.html new file mode 100644 index 0000000000..8111d49c37 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/BestBuy/SignIn.html @@ -0,0 +1,21 @@ + + + + Sign In to BestBuy.com + + +
+
+ + +
+
+ +
+ +
+
+ +
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_BillingPaymentInfo.html b/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_BillingPaymentInfo.html new file mode 100644 index 0000000000..35adee68b9 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_BillingPaymentInfo.html @@ -0,0 +1,469 @@ + + + + + + Checkout + + + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
The billing address above must match what appears on this credit card's statement.
+
+
+ + +
+
+ + + +
+
+ + + + + +
+
+ + +
+ + +
+
+
+ What is a CVV? +
+ For Visa, MasterCard & Discover, the three digits on the back of your card. +
+ For American Express, the 4 digits on the front of your card. +
+
+
+
+
+
+
+
+
+ +
+ + Verisign Secured + +
+
+ + BBB Accredited Busines + +
+
+
+
+
    +
  • + Edit +
    +
    +
  • +
  • + Edit +
    +
    Shipping Method
    +
    +
    UPS Ground (2-3 days)
    +
    2-3 business days
    +
    $19.99
    +
    +
    +
  • +
  • + Edit +
    +
    + Billing Address +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +, + + + + +
    +
    +
    Payment Method
    +
    +
    +
    +
  • +
  • +
  • +
+
+ +
+ +
+
+ + +
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_Logon.html b/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_Logon.html new file mode 100644 index 0000000000..6ee46c8873 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_Logon.html @@ -0,0 +1,118 @@ + + + + + + Logon Checkout + + + +
+
+ + + +
+
+ + +
+

You don't need an account to place an order but you will have the option to create one after completing your purchase.

+
+
+
+

Click or touch the House +

+
+
+ +
+
+
+ + +
+
+ + + +
+ +! The validation code entered is incorrect +
+
+ +
+
+ + +
+
+
+
+
+ User Name + Forgot user name? +
+ +
+
+
+ + +
+ + + + +
+
+
+ Password + Forgot password? +
+ +
+
+
+
+
+ ! You have entered an invalid username and/or password. Please re-enter your information. +
+
+ ! + + +
+ +
+
+ + + + + +
+
+ +
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_ShippingInfo.html b/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_ShippingInfo.html new file mode 100644 index 0000000000..d461a0050b --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_ShippingInfo.html @@ -0,0 +1,376 @@ + + + + + + Checkout + + + +
+
+ + + +
+
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
We will only contact you about your order and shipping.
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
    +
  • + Edit +
    +
    + Address: +
    +
    +
    + + + + +
    +
    + + +
    +
    +
    +
    +
    +
    + +, + + + + +
    +
    +
    +
    + Contact Info: +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
  • +
  • + Edit +
    +
    Shipping Method
    +
    +
    -
    +
    -
    +
    -
    +
    +
    +
  • +
  • + Edit +
    +
    + Billing Address +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +, + + + + +
    +
    +
    Payment Method
    +
    +
    +
    +
  • +
  • +
  • +
+
+
+ +
+
+ +
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/CostCo/Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/CostCo/Payment.html new file mode 100644 index 0000000000..8b11a6a49e --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/CostCo/Payment.html @@ -0,0 +1,892 @@ + + + + + + Costco - Payment + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + +  ? + +
+
+
+ + +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + +
+
+ + +
+
+ + +  ? + +
+         More Info - Costco Cash Card + + Costco Cash Card Number + This image highlights the unique number + cashcard-us.gif + used to identify your Costco Cash card. + Pin Number + This number is used to access your + + Costco Cash card. The image shows + where this number is located. + @ 1998-2016 Costco Wholesale Corporation. All rights reserved. +
+
+
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ +

+* Required fields

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + + +
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +

+* Required fields

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + +
+ +
+ +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
[rx-DefaultAddrConfirm]
+
+
    +
  • + You are changing your Costco Default Shipping Address.  All future orders from Costco.com, including Pharmacy Prescription Orders, will be sent to this Address. +
  • +
+
+
+
+
+
+
+ +
+
+
+
+
+ + + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/CostCo/ShippingAddress.html b/browser/extensions/formautofill/test/fixtures/third_party/CostCo/ShippingAddress.html new file mode 100644 index 0000000000..1740ebb84d --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/CostCo/ShippingAddress.html @@ -0,0 +1,527 @@ + + + + + + + Shipping + + + + + + +
+ + + + + + +
+
+ + + + + + +
+
+
+ +

+ * Required fields

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + + +
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +

+* Required fields

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + +
+ +
+ +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ [rx-DefaultAddrConfirm] +
+
+
    +
  • You are changing your Costco Default Shipping Address.  All future orders from Costco.com, including Pharmacy Prescription Orders, will be sent to this Address. +
  • +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+
+
+ + +
+ + +
+
+ + + + + + +
+ + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/CostCo/SignIn.html b/browser/extensions/formautofill/test/fixtures/third_party/CostCo/SignIn.html new file mode 100644 index 0000000000..afcd5fe6f0 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/CostCo/SignIn.html @@ -0,0 +1,374 @@ + + + + + + Sign In + + + + + + +
+ + +
+ + + + + +
+
+
+
+
+ +
+ +
+ + +
+
+ + + + +
+
+
+
+
  • + +
  • +
    +
    +
    + + +
    +
    +
    + + + + + + + +

    Please provide your email address and password to access your account.† +

    +
    + + +
    +
    +
    + + +

    Passwords are case sensitive.

    +
    +
    +
    + + +
    + +
    + +
    +
    +
    + + + + + + + + + + +

    To reset your password, enter the email address associated with your Costco.com account. Instructions to create a new password will be sent to your address. +

    +
    + + +
    + +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + +

    Enter your email address and create a password below to register.† +

    +
    +* Required fields +
    +
    + + +
    +
    + + +
    + +
    +

    Password must meet the following:

    +
    +
      +
    • Use between 8 and 20 characters
    • +
    • Include at least one letter
    • +
    • Does not contain blank spaces or the following special characters: < > " \ . +
    • +
    • Passwords match
    • +
    +
    +

    Password Strength : + +

    +
      +
    • Too Short
    • +
    • Weak
    • +
    • Fair
    • +
    • Good
    • +
    • Strong
    • +
    +

    +

    +

    To improve strength, increase password length and use capital letters, numbers, and special characters + (except < > " \ .) +

    +
    +
    +
    +
    + + +
    +
    + + +
    +

    +Non-members may be assessed an additional surcharge. The surcharge does not apply to prescription items. Executive Members need to provide a membership number to receive credit for their 2% rebate. +

    +
    + + +
    + +
    + +
    +
    + +
    +
    + ???LANGUAGE_REGION_MODAL_TITLE??? +
    +
    +
    +

    ???LANGUAGE_REGION_MODAL_CHOOSE_LANGUAGE???

    + +
    +
    +
    +

    ???LANGUAGE_REGION_MODAL_CHOOSE_REGION???

    +
    + + + + + + + +
    +
    + + + + + + +
    +
    +
    + +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/DirectAsda/Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/DirectAsda/Payment.html new file mode 100644 index 0000000000..5a5b066f50 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/DirectAsda/Payment.html @@ -0,0 +1,90 @@ + + + + + + + + Direct.asda.com + + + +
    +
    +
    +
    Please enter a card number +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + ? + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    / +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Ebay/Checkout_Payment_FR.html b/browser/extensions/formautofill/test/fixtures/third_party/Ebay/Checkout_Payment_FR.html new file mode 100644 index 0000000000..83717a6c17 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Ebay/Checkout_Payment_FR.html @@ -0,0 +1,135 @@ + + + + + + + + Checkout Payment - Ebay - FR + + + +
    +
    + +
    +

    Finalisation de l'achat

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    Visa, Mastercard ou Discover
    +
      +
    • Il s'agit du numéro à 3 chiffres situé au + verso de votre carte, à côté de l'emplacement réservé à la + signature.
    • +
    +
    American Express
    +
      +
    • Il s'agit du numéro à 4 chiffres situé au + recto de votre carte au-dessus de son numéro.
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/GlobalDirectAsda/Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/GlobalDirectAsda/Payment.html new file mode 100644 index 0000000000..7eee1b1215 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/GlobalDirectAsda/Payment.html @@ -0,0 +1,154 @@ + + + + + + + + Global.direct.asda.com + + + +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    Month
    +
    +
    + +
    +
    +
    +
    Year
    +
    +
    +
    +
    +
    + +
    + +
    + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/Checkout_ShippingPayment.html b/browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/Checkout_ShippingPayment.html new file mode 100644 index 0000000000..1825350651 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/Checkout_ShippingPayment.html @@ -0,0 +1,381 @@ + + + + + + + The Home Depot - Checkout + + + + +
    + + +
    +
    +
    +
    + + + + + + +
    +
    + + + + + + +
    +
    +
    +
    + + + + + + +
    +
    +
    + +
    +
    +
    +Create an account to track your order history and check out faster - all we need is a password.
    +
    +

    Check out faster, access past orders, and organize products into lists.

    +
    +
    +
    +
    + + + + + + + + +
    +
    + + + + + + + + +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + Passwords are case sensitive and must be at least 8 characters. +
    + Create a strong password by: +
      +
    • Including numbers or symbols
    • +
    • Mixing upper/lowercase
    • +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    + + + + + + +
    +
    +
    +
    +
    +
    + +
    + + +
    + +
    +
    +
    +
    +
    +
    +Add an apartment, suite, building, etc. +
    +
    +
    + + + + +
    +
    + + + +MOUNTAIN VIEW, CA + +
    + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    + Payment +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + + + + + +
    +
    +
    +
    +
    + Expiration + + + +
    +
    +
    +
    + + + +
    +
    +
    +
    + + + + + + +
    +
    +
    + Apply a Gift Card +  |  + Have a PO/Job Code for this order? +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +Have a promo code? +
    +
    +
    + + + + +
    +
    +
    + + + + + +Apply + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/SignIn.html b/browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/SignIn.html new file mode 100644 index 0000000000..b741ce5f0c --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/SignIn.html @@ -0,0 +1,83 @@ + + + + + The Home Depot - SignIn + + + + + + + +
    + + + + + + + + + + + +
    + + + + +
    +
    +

    You will have the opportunity to create an account and track your order once you complete your checkout. +

    +

    +

    +
    +
    + +
    +
    +
    + + +

    I'm a Returning Customer +

    +
    + Your sign in is incorrect. Please enter your email address or password. Note: One more invalid attempt will lock your account. +
    +
    + We periodically require password updates. Please reset your password or continue as a guest. +
    +
    + + + + +
    +
    + + + + + +
    +
    +
    + +
    + +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Lufthansa/Checkout_Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/Lufthansa/Checkout_Payment.html new file mode 100644 index 0000000000..38facb8009 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Lufthansa/Checkout_Payment.html @@ -0,0 +1,23 @@ + + + + + + + Document + + +
    +
    Payment line option Credit card
     
    - Please select -
    Click to expand
     
    Enter your card numberInformation ||| 
    Expiry date (month/year) 
    01
    Click to expand
    2021
    Click to expand
     
    Security Code (CVC/CVS) The security code of your Mastercard/ Visa consists of 3 digits and is printed on the backside of your card close to/ within the signature field. For American Express it is 4 digits printed on the front side.  
     
     

      Your payment details will be processed applying highest security standards.

    In the next step an input window of your bank might open in which you will need to enter your TAN/mTAN belonging to the card in order to confirm the payment. For a smooth payment authentication, please make sure that your pop-up blocker is deactivated and your card is enabled for this process.

     
     
    + +
    +
    + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Lush/index.html b/browser/extensions/formautofill/test/fixtures/third_party/Lush/index.html new file mode 100644 index 0000000000..a77fd13132 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Lush/index.html @@ -0,0 +1,421 @@ + + + + + + + Step 1: Choose your Payment Method + + + + + + + + + + + + + + + + + +
    +
    +
    +
      +
    1. + + + Addresses +
    2. + +
    3. + + + Payment +
    4. + +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    + +
    +
    + +
    +
    + + + + +

    Step 1: Please select your payment method

    + +
    + Total payment amount GBP 31.40 +
    + + +
      + + + + +
    • + + + + + +
      + + + + +

      + + + + + + + + + + + + + + + + +
      Name
      Card Number
      +
      +
      + If there is not enough balance on my card, pay the rest of the payment amount with an other payment method. +
      + +
      + +

      + + + + +
    • + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      Card Number
      Card Holder Name
      + +
      Card Expiry Date
      +
      + +  /  + +
      +
      CVC/CVV/CID
      + +
      + + + + + + + + + + + +
    • + + + + + +
      + + + + + + + + + + + +
      + Pay using your PayPal account. You will be redirected to the PayPal system to complete the payment. +
      + +
      +
      +
    • + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + + +
    +
    + + + + + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_Payment.html new file mode 100644 index 0000000000..85717a4eaa --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_Payment.html @@ -0,0 +1,474 @@ + + + + + Macy's Checkout + + + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + + +
    +
    +
    +
    + Note: PayPal can't be used with Gift Cards, Reward Cards and Credit Cards. + Plenti points can be earned but not used with PayPal. +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Secure payment + more info +

    +
    +
    +
    +
    + + +
    Your Shipping, Plenti, and Gift Card information can be found and verified at the top of this page"
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    Please enter the 4 digit security code on the front of your credit card
    +
    Please enter the 3 digit security code on the back of your credit card
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + Note: PayPal can't be used with Gift Cards, Reward Cards and Credit Cards. + Plenti points can be earned but not used with PayPal. +
    +
    +
    +
    + You will login on PayPal's site on the next page and review your order, then you will finish the transaction at macy's.com +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + We'll only contact you if we have questions about this order. +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_ShippingAddress.html b/browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_ShippingAddress.html new file mode 100644 index 0000000000..7ed68344fa --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_ShippingAddress.html @@ -0,0 +1,439 @@ + + + + + Macy's Checkout + + + + + + +
    +
    +
    +
    +
    + + +Please enter a first name. +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + Shipping method +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    $10.95 +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    $19.95 +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    $29.95 +
    +
    +
    +
    +

    +Note: We'll send you an email to schedule your delivery. +

    +

    +Note: Some items in your order may ship separately. Transit time is the time between leaving our fulfillment center & delivery to you. +

    +
    +
    +
    +
    +
    +
    + + Gift Options + +
    +
    +
    +
    + +
    + "Selecting this checkbox will expand additional gift options" +
    + + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + Write a personal message. We'll print it on a card & send it along with the order. +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Macys/SignIn.html b/browser/extensions/formautofill/test/fixtures/third_party/Macys/SignIn.html new file mode 100644 index 0000000000..51dff05d04 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Macys/SignIn.html @@ -0,0 +1,208 @@ + + + + + Sign In - Macy's Checkout + + + + + + +
    +
    +
      +
    • +
      + +
      +
      + +
      +
       
      +
    • +
    • +
      + +
      +
      + +
      +
       
      +
    • +
    +
    +
    +Password is case sensitive +
    + +
    + +
    +
    +
    +
    +
    +
    +
      +
    • + + +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
      +
    • + + + + + +
    • +
    • + enter the letters in the below field +
      + +new_image + +
      +
    • +
    • + + + +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
      +
    • + + + +
    • +
    • +
      +
      +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
      +
    • +
      +
      + +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      +
    • +
    • +
      +
      + +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + +
    • +
    +
    +
    + + + + + + + + + + + + + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html new file mode 100644 index 0000000000..d44bc01b70 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html @@ -0,0 +1,1074 @@ + + + + + Newegg.com - Billing Info + + + + + + + + + +
    +
    + Redeem Newegg gift cards + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + + +
    +
    + EggPoints +
    +
    +
      +
    • +
      +
      +
      +
      +
      +
      +
      +Missing Information Card Number and Security Code fields cannot be empty. Please enter valid information and try again.
      +
      +
      +
      +
      +
    • +
    +
    + Payment Methods + + +
    +

    Some payment methods may not be eligible for your order. Please review the full list by clicking here for payment restrictions.

    +
    + + +
    +
    +
      +
    • + + +
      +
      +
      + Newegg Store Credit Card +

      +

      +
      +
      + Newegg Store Credit Card +
      +
      +
      +
      +
      + + +
      +
      + + +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      Do not have a Newegg Store Credit Card? Learn More +
      +
      +
    • +
    • + + +
      +
      +
      + Amex Express Checkout Button +

      +

      +
      +
      + Checkout faster with American Express +
      +
      +
      +
    • +
    • + + +
      +
      +
      + Bitcoin accepted here +

      +

      +
      +
      + Bitcoin is the safest and most secure way to pay online. +
        +
      • No identity theft risk; no payment information is ever stored.
      • +
      • Your payment completes immediately.
      • +
      • +Learn more + +
      • +
      +
      +
      +
      +
      +
      +
      +
      +NOTE: Please note that when using Bitcoin as your payment method, once you have clicked the "bitcoin checkout now" button, you will have only 15 minutes to complete your payment. If you are unable to complete your payment, you will have two options: You can try again later to place a new order or you can change your payment method later from Order History in My Account.
      +
      + All orders fully paid by Bitcoin are final and cannot be returned for Bitcoin or hard currency. All returns will be made in the form of a Newegg Gift Card. All returns follow our return policy. +
      +
      +
      +
      +
      +
      +
    • + +
    • + + +
      +
      +
      + MasterPass +

      +

      +
      +
      + MasterPass is a free service that is a fast, simple and safe way to check out online. It cuts down on the time and effort it takes to buy the things you want and need. And because it's from MasterCard, you can trust that it's secure. + +
        +
      • + When you are ready to check out online, click on the "Buy with MasterPass" button. +
      • +
      • + Next, unlock your MasterPass account to choose your payment method and shipping address. +
      • +
      • + Then simply confirm your purchase, and you are done! +
      • +
      • + Use it with all major credit, debit, and prepaid cards. +
      • +
      • + Use it to easily store all your cards and addresses in one place. +
      • +
      • + Use it on all your connected devices. +
      • +
      +
      +
      +
      +
    • +
    • + + +
      +
      +
      + +Visa Checkout + + Visa Checkout + +

      +

      +
      +
      +
      +
    • +
    • + + +
      +
      +
      + newegg +
        +
      • + Speed through checkout. +
      • +
      • + PayPal is the safer, easier way to pay. +
      • +
      • + + What is PayPal? + +
      • +
      +

      +

      +
      +
      + + PayPal is the safer, easier way to pay + +
        +
      • + Never expose your credit card number. +
      • +
      • + Speed through checkout all over the web. One account, one password - no need to retype your shipping or financial information. +
      • +
      +
      +
      +
      +
    • +
    • + + +
      +
      +
      +
      + +
      +
      +
      + +
      + +
      +
      +
      +
      +
      +

      + All major credit cards accepted: + + Major Cards Accepted + Major Cards Accepted + Major Cards Accepted + Major Cards Accepted + +

      +
      +
      + + +
      +
      + + + Major Cards Accepted +
      +
      + + + +
      +
      + + + + cvv2 + +
      +
      +
      + +
      +
      + +
      +
      + +
      +
      +
      +
      +
      + + + + + + + + + + + + + + + + + + + +
      +
      +
      +
    • +
    + +
    +
    + Billing Address + + +
    +
      +
    • + + +
    • +
    • +
    • +
    • +
      + + + +
        +
      • + + +
      • +
      • + + +
      • +
      • + + +
      • +
      • +
          +
        • +
          + + +
          +
        • +
        • +
          + +
          + +
          +
          +
          + + +
          +
        • +
        • + + +
        • +
        • + + +
        • + + +
        +
      • +
      +
      +
    • + + + + + + + + + + + + +
    + + + + + + + + + + + +
    +
    +
    +
    +
    +
      +
    • + + +
    • +
    • + + +
    • +
    • + + + Major Cards Accepted +
    • +
    • + + + +
    • +
    • + +
    • +
    • +
    • +
    +
    + + + + + + + +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
      +
    • + + +
    • +
    • + + +
    • +
    • +
    • + + +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/Login.html b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/Login.html new file mode 100644 index 0000000000..a5f149b683 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/Login.html @@ -0,0 +1,156 @@ + + + + + Newegg.com - Login + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    +
    + + CONTINUE AS A GUEST + +
    +
    + + + + + + +
      +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • +
    • +
    • + + +
    • +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/ShippingInfo.html b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/ShippingInfo.html new file mode 100644 index 0000000000..14bd9502af --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/ShippingInfo.html @@ -0,0 +1,270 @@ + + + + + Newegg.com - Shipping Info + + + + + + + + + +
    + + + + +
      +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • +
        +
      • +
        + + +
        +
      • +
      • +
        + +
        + +
        +
        +
        + + +
        +
      • +
      • + + +
      • +
      • + + +
      • +
      • + +
      • + +
      +
    • +
    + +
    + + + +
      +
    • + + +
      +
      +
    • +
    +
    + + +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/Payment.html new file mode 100644 index 0000000000..06b9f8e763 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/Payment.html @@ -0,0 +1,672 @@ + + + + + + + + + + + + + + + + + Office Supplies, Furniture, Technology at Offic Depot + + + +
    + + + + + + + + + + + + + +
    +
    +
    +
    + + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +Need Help? +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +

    + +

    + + 87 +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + + + + + +
    + + + + + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/ShippingAddress.html b/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/ShippingAddress.html new file mode 100644 index 0000000000..849e3be495 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/ShippingAddress.html @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + Office Supplies, Furniture, Technology at Office Depot + + + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + + +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + +
    + + + + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + +
    +
    + + +
    +
    +
    +
    + + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/SignIn.html b/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/SignIn.html new file mode 100644 index 0000000000..70b55feddd --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/SignIn.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + Office Supplies, Furniture, Technology at Office Depot + + + +
    + + + + + +
    + + + +
    +
    + + + +Forgot login name/password? + +
    +
    + Keep me logged in +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/QVC/PaymentMethod.html b/browser/extensions/formautofill/test/fixtures/third_party/QVC/PaymentMethod.html new file mode 100644 index 0000000000..d8878692c3 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/QVC/PaymentMethod.html @@ -0,0 +1,527 @@ + + + + + + + + + Payment Method + + +
    + + + + + + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    +
    +
      +
    • + + +
    • +
    • + + +
    • +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + +
    + + + (formerly Bill Me Later®) +
    +
    +
    + + +
    +
    + + +
    +
    + XXX-XX- +
    +
    + + + + + + +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    + +      +
    +
    +
    + + + + + +
    + + +
    +
    +
    +
    +   +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + + + + + + Enter Another Card + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + Continue Checkout + + + Continue Checkout + + + + + Edit Shopping cart + +
    +
    +
    +
    + + * + + + + *You're signing up to receive QVC promotional email. +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/QVC/SignIn.html b/browser/extensions/formautofill/test/fixtures/third_party/QVC/SignIn.html new file mode 100644 index 0000000000..a056ccfc5c --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/QVC/SignIn.html @@ -0,0 +1,80 @@ + + + + + + + + QVC.com Sign In + + +
    +
    + + + + + + +
    +
    +
    + +
    + + + + + + + + +
    +
    + + +
    + + +
    +
    +
    +
    + + + +
    +
    +
    +
    + + Create Password + + + + Continue + + +
    +
    + + Sign In + + +
    +
    +
    + +
    +
    +
    + +
    + + Continue + + +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/QVC/YourInformation.html b/browser/extensions/formautofill/test/fixtures/third_party/QVC/YourInformation.html new file mode 100644 index 0000000000..df5fdc2200 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/QVC/YourInformation.html @@ -0,0 +1,522 @@ + + + + + + + + + Payment Method + + +
    + + + + + + + + + + + + + + + + + + + +
    +
    +
      +
    • + + +
    • +
    • + + +
    • +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + XXX-XX- +
    +
    + + + + + + +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    + +      +
    +
    +
    + + + + + +
    + + +
    +
    +
    +
    +   +
    +
    +
    + + + Add My QCard + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + + + + + + Enter Another Card + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + Continue Checkout + + + Continue Checkout + + + + + Edit Shopping cart + +
    +
    +
    +
    + + * + + + + *You're signing up to receive QVC promotional email. +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/README b/browser/extensions/formautofill/test/fixtures/third_party/README new file mode 100644 index 0000000000..ca4750ec08 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/README @@ -0,0 +1,4 @@ +This directory contains pages downloaded from the web for the purpose of testing +Form Autofill against pages from the real world. These files are not made +available under an open source license. + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Sears/PaymentOptions.html b/browser/extensions/formautofill/test/fixtures/third_party/Sears/PaymentOptions.html new file mode 100644 index 0000000000..160823920e --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Sears/PaymentOptions.html @@ -0,0 +1,566 @@ + + + + + + + + + + + Payment Options | Sears PartsDirect + + + + +
    +
    + + + + + + +
    +
    +
    + + + + + + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + + + + + + +
    + + +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + + + +
    +
    +
    +
    +
    + + +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Sears/ShippingAddress.html b/browser/extensions/formautofill/test/fixtures/third_party/Sears/ShippingAddress.html new file mode 100644 index 0000000000..a9d1f5cd0f --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Sears/ShippingAddress.html @@ -0,0 +1,447 @@ + + + + + + + + + + + Shipping address | Sears PartsDirect + + + + +
    +
    + + + + + + +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    +

    +

    +
    + + + +
    +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    24 character limit
    +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + + + + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + + +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + + + + +
    + + +
    +
    + + +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic.html b/browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic.html new file mode 100644 index 0000000000..cf9e892cb2 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic.html @@ -0,0 +1,117 @@ + + + + + It's easy to find the Office Supplies, Copy Paper, + Furniture, Ink, Toner, Cleaning Products, Electronics and the + Technology you need | Staples® + + + + + + + + + + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic_ac_on.html b/browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic_ac_on.html new file mode 100644 index 0000000000..d3ba1116aa --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic_ac_on.html @@ -0,0 +1,117 @@ + + + + + It's easy to find the Office Supplies, Copy Paper, + Furniture, Ink, Toner, Cleaning Products, Electronics and the + Technology you need | Staples® + + + + + + + + + + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling.html b/browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling.html new file mode 100644 index 0000000000..37dadeb514 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling.html @@ -0,0 +1,99 @@ + + + + + It's easy to find the Office Supplies, Copy Paper, + Furniture, Ink, Toner, Cleaning Products, Electronics and the + Technology you need | Staples® + + + + + + + + + + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +

    Purchase Order # (optional) Add +

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    +By placing your order, you agree to + Staples + +Terms & + Conditions. + +

    +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling_ac_on.html b/browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling_ac_on.html new file mode 100644 index 0000000000..98c9fb8555 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling_ac_on.html @@ -0,0 +1,98 @@ + + + + + It's easy to find the Office Supplies, Copy Paper, + Furniture, Ink, Toner, Cleaning Products, Electronics and the + Technology you need | Staples® + + + + + + + + + + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +

    Purchase Order # (optional) Add +

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    +By placing your order, you agree to + Staples + +Terms & + Conditions. + +

    +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Checkout.html b/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Checkout.html new file mode 100644 index 0000000000..ee5ded483e --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Checkout.html @@ -0,0 +1,243 @@ + + + + + + + + + + + + +
    +

    Enter new zip code:

    + +
    +

    + +

    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    *required field
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +By clicking Create Account, you acknowledge you + have read and agreed to our +Terms of + Use +and +Privacy + Policy +. + +
    + +
    +
    + + +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Payment.html b/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Payment.html new file mode 100644 index 0000000000..f6a46387b2 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Payment.html @@ -0,0 +1,235 @@ + + + + + + + + + Checkout + + + + + + + + + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    * required field
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + + + +
    +  /  +
    + + + +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +

    +22F., No.55, Haiiu 1st Rd., Bafu Dist., +
    + +San Bruno +, + + +CA + +94066 +

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Shipping.html b/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Shipping.html new file mode 100644 index 0000000000..5f07a05bab --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/third_party/Walmart/Shipping.html @@ -0,0 +1,234 @@ + + + + + + + + + Checkout + + + + + + + + + + + +
    +

    Enter new zip code:

    + +
    +

    + +

    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    *required field
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/without_autocomplete_address_basic.html b/browser/extensions/formautofill/test/fixtures/without_autocomplete_address_basic.html new file mode 100644 index 0000000000..a69b65f1d7 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/without_autocomplete_address_basic.html @@ -0,0 +1,26 @@ + + + + + Form Autofill Address Demo Page + + +

    Form Autofill Address Demo Page (without autocomplete attribute)

    +
    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    + + +

    +
    + + diff --git a/browser/extensions/formautofill/test/fixtures/without_autocomplete_creditcard_basic.html b/browser/extensions/formautofill/test/fixtures/without_autocomplete_creditcard_basic.html new file mode 100644 index 0000000000..b73645f718 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/without_autocomplete_creditcard_basic.html @@ -0,0 +1,53 @@ + + + + + Form Autofill Credit Card Demo Page + + +

    Form Autofill Credit Card Demo Page (without Autocomplete attribute)

    + +
    +

    +

    +

    +

    +

    +

    +

    + + +

    +
    + + +
    +

    +
    +
    +

    +

    +

    +

    +

    +

    + + +

    +
    + + diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini new file mode 100644 index 0000000000..d7a7ea2fd8 --- /dev/null +++ b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini @@ -0,0 +1,26 @@ +[DEFAULT] +prefs = + extensions.formautofill.creditCards.supported=on + extensions.formautofill.creditCards.enabled=true + extensions.formautofill.reauth.enabled=true +support-files = + !/toolkit/components/satchel/test/satchel_common.js + ../../../../../../toolkit/components/satchel/test/parent_utils.js + !/toolkit/components/satchel/test/parent_utils.js + !/browser/extensions/formautofill/test/mochitest/formautofill_common.js + !/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js +skip-if = xorigin + toolkit == 'android' # bug 1730213 + +[test_basic_creditcard_autocomplete_form.html] +scheme=https +[test_clear_form.html] +scheme=https +[test_clear_form_expiry_select_elements.html] +scheme=https +[test_creditcard_autocomplete_off.html] +scheme=https +[test_preview_highlight_with_multiple_cc_number_fields.html] +scheme=https +[test_preview_highlight_with_site_prefill.html] +scheme=https diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html new file mode 100644 index 0000000000..0764bef0eb --- /dev/null +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html @@ -0,0 +1,251 @@ + + + + + Test basic autofill + + + + + + + +Form autofill test: simple form credit card autofill + + + +

    + +
    + +
    +

    This is a basic form.

    +

    +

    +

    +

    +

    +

    +
    +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
    new file mode 100644
    index 0000000000..3d8049f053
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
    @@ -0,0 +1,205 @@
    +
    +
    +
    +  
    +  Test form autofill - clear form button
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: clear form button
    +
    +
    +
    +

    + +
    + +
    +

    This is a basic form.

    +

    +

    +

    +

    + +

    +

    +

    +

    +

    +
    + +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
    new file mode 100644
    index 0000000000..4fc989a36e
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
    @@ -0,0 +1,211 @@
    +
    +
    +
    +  
    +  Test form autofill - clear form button with select elements
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: clear form button with select elements.
    +
    +
    +
    +

    + +
    + +
    +

    This is a basic form.

    +

    +

    +

    +

    + +

    +

    + +

    + +

    +

    +
    + +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
    new file mode 100644
    index 0000000000..225d828ccb
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
    @@ -0,0 +1,96 @@
    +
    +
    +
    +  
    +  Test basic autofill
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: simple form credit card autofill
    +
    +
    +
    +

    + +
    +
    +

    This is a Credit Card form with autocomplete="off" cc-name field.

    +

    +

    +
    +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
    new file mode 100644
    index 0000000000..c39877d1b7
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
    @@ -0,0 +1,174 @@
    +
    +
    +
    +  
    +  Test form autofill - preview and highlight with multiple cc number fields
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: preview and highlight multiple cc number fields
    +
    +
    +

    +
    + +
    +

    This is a basic credit card form.

    +

    card number subsection 1:

    +

    card number subsection 2:

    +

    card number subsection 3:

    +

    card number subsection 4:

    +

    cardholder name:

    +

    expiration month:

    +

    expiration year:

    +
    +
    +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
    new file mode 100644
    index 0000000000..090eb9290e
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
    @@ -0,0 +1,110 @@
    +
    +
    +
    +  
    +  Test form autofill - preview and highlight with site prefill
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: preview and highlight field that has been filled by site
    +
    +
    +

    +
    + +
    +

    This is a basic credit card form.

    +

    card number:

    +

    cardholder name:

    +

    expiration month:

    +

    expiration year:

    +
    +
    +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_common.js b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
    new file mode 100644
    index 0000000000..d65f0beb49
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
    @@ -0,0 +1,478 @@
    +/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
    +/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
    +/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
    +/* eslint-disable no-unused-vars */
    +
    +"use strict";
    +
    +let formFillChromeScript;
    +let defaultTextColor;
    +let defaultDisabledTextColor;
    +let expectingPopup = null;
    +
    +const { FormAutofillUtils } = SpecialPowers.ChromeUtils.importESModule(
    +  "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
    +);
    +
    +async function sleep(ms = 500, reason = "Intentionally wait for UI ready") {
    +  SimpleTest.requestFlakyTimeout(reason);
    +  await new Promise(resolve => setTimeout(resolve, ms));
    +}
    +
    +async function focusAndWaitForFieldsIdentified(
    +  input,
    +  mustBeIdentified = false
    +) {
    +  info("expecting the target input being focused and indentified");
    +  if (typeof input === "string") {
    +    input = document.querySelector(input);
    +  }
    +  const rootElement = input.form || input.ownerDocument.documentElement;
    +  const previouslyFocused = input != document.activeElement;
    +
    +  input.focus();
    +
    +  if (mustBeIdentified) {
    +    rootElement.removeAttribute("test-formautofill-identified");
    +  }
    +  if (rootElement.hasAttribute("test-formautofill-identified")) {
    +    return;
    +  }
    +  if (!previouslyFocused) {
    +    await new Promise(resolve => {
    +      formFillChromeScript.addMessageListener(
    +        "FormAutofillTest:FieldsIdentified",
    +        function onIdentified() {
    +          formFillChromeScript.removeMessageListener(
    +            "FormAutofillTest:FieldsIdentified",
    +            onIdentified
    +          );
    +          resolve();
    +        }
    +      );
    +    });
    +  }
    +  // In order to ensure that "markAsAutofillField" is fully executed, a short period
    +  // of timeout is still required.
    +  await sleep(300, "Guarantee asynchronous identifyAutofillFields is invoked");
    +  rootElement.setAttribute("test-formautofill-identified", "true");
    +}
    +
    +async function setInput(selector, value, userInput = false) {
    +  const input = document.querySelector("input" + selector);
    +  if (userInput) {
    +    SpecialPowers.wrap(input).setUserInput(value);
    +  } else {
    +    input.value = value;
    +  }
    +  await focusAndWaitForFieldsIdentified(input);
    +
    +  return input;
    +}
    +
    +function clickOnElement(selector) {
    +  let element = document.querySelector(selector);
    +
    +  if (!element) {
    +    throw new Error("Can not find the element");
    +  }
    +
    +  SimpleTest.executeSoon(() => element.click());
    +}
    +
    +// The equivalent helper function to getAdaptedProfiles in FormAutofillHandler.jsm that
    +// transforms the given profile to expected filled profile.
    +function _getAdaptedProfile(profile) {
    +  const adaptedProfile = Object.assign({}, profile);
    +
    +  if (profile["street-address"]) {
    +    adaptedProfile["street-address"] = FormAutofillUtils.toOneLineAddress(
    +      profile["street-address"]
    +    );
    +  }
    +
    +  return adaptedProfile;
    +}
    +
    +async function checkFieldHighlighted(elem, expectedValue) {
    +  let isHighlightApplied;
    +  await SimpleTest.promiseWaitForCondition(function checkHighlight() {
    +    isHighlightApplied = elem.matches(":autofill");
    +    return isHighlightApplied === expectedValue;
    +  }, `Checking #${elem.id} highlight style`);
    +
    +  is(isHighlightApplied, expectedValue, `Checking #${elem.id} highlight style`);
    +}
    +
    +async function checkFieldPreview(elem, expectedValue) {
    +  is(
    +    SpecialPowers.wrap(elem).previewValue,
    +    expectedValue,
    +    `Checking #${elem.id} previewValue`
    +  );
    +  let isTextColorApplied;
    +  await SimpleTest.promiseWaitForCondition(function checkPreview() {
    +    const computedStyle = window.getComputedStyle(elem);
    +    const actualColor = computedStyle.getPropertyValue("color");
    +    if (elem.disabled) {
    +      isTextColorApplied = actualColor !== defaultDisabledTextColor;
    +    } else {
    +      isTextColorApplied = actualColor !== defaultTextColor;
    +    }
    +    return isTextColorApplied === !!expectedValue;
    +  }, `Checking #${elem.id} preview style`);
    +
    +  is(isTextColorApplied, !!expectedValue, `Checking #${elem.id} preview style`);
    +}
    +
    +async function checkFormFieldsStyle(profile, isPreviewing = true) {
    +  const elems = document.querySelectorAll("input, select");
    +
    +  for (const elem of elems) {
    +    let fillableValue;
    +    let previewValue;
    +    let isElementEligible =
    +      FormAutofillUtils.isCreditCardOrAddressFieldType(elem) &&
    +      FormAutofillUtils.isFieldAutofillable(elem);
    +    if (!isElementEligible) {
    +      fillableValue = "";
    +      previewValue = "";
    +    } else {
    +      fillableValue = profile && profile[elem.id];
    +      previewValue = (isPreviewing && fillableValue) || "";
    +    }
    +    await checkFieldHighlighted(elem, !!fillableValue);
    +    await checkFieldPreview(elem, previewValue);
    +  }
    +}
    +
    +function checkFieldValue(elem, expectedValue) {
    +  if (typeof elem === "string") {
    +    elem = document.querySelector(elem);
    +  }
    +  is(elem.value, String(expectedValue), "Checking " + elem.id + " field");
    +}
    +
    +async function triggerAutofillAndCheckProfile(profile) {
    +  let adaptedProfile = _getAdaptedProfile(profile);
    +  const promises = [];
    +  for (const [fieldName, value] of Object.entries(adaptedProfile)) {
    +    info(`triggerAutofillAndCheckProfile: ${fieldName}`);
    +    const element = document.getElementById(fieldName);
    +    const expectingEvent =
    +      document.activeElement == element ? "input" : "change";
    +    const checkFieldAutofilled = Promise.all([
    +      new Promise(resolve => {
    +        let beforeInputFired = false;
    +        let hadEditor = SpecialPowers.wrap(element).hasEditor;
    +        element.addEventListener(
    +          "beforeinput",
    +          event => {
    +            beforeInputFired = true;
    +            is(
    +              event.inputType,
    +              "insertReplacementText",
    +              'inputType value should be "insertReplacementText"'
    +            );
    +            is(
    +              event.data,
    +              String(value),
    +              `data value of "beforeinput" should be "${value}"`
    +            );
    +            is(
    +              event.dataTransfer,
    +              null,
    +              'dataTransfer of "beforeinput" should be null'
    +            );
    +            is(
    +              event.getTargetRanges().length,
    +              0,
    +              'getTargetRanges() of "beforeinput" should return empty array'
    +            );
    +            is(
    +              event.cancelable,
    +              SpecialPowers.getBoolPref(
    +                "dom.input_event.allow_to_cancel_set_user_input"
    +              ),
    +              `"beforeinput" event should be cancelable on ${element.tagName} unless it's suppressed by the pref`
    +            );
    +            is(
    +              event.bubbles,
    +              true,
    +              `"beforeinput" event should always bubble on ${element.tagName}`
    +            );
    +            resolve();
    +          },
    +          { once: true }
    +        );
    +        element.addEventListener(
    +          "input",
    +          event => {
    +            if (element.tagName == "INPUT" && element.type == "text") {
    +              if (hadEditor) {
    +                ok(
    +                  beforeInputFired,
    +                  `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
    +                );
    +              } else {
    +                ok(
    +                  beforeInputFired,
    +                  `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
    +                );
    +              }
    +              ok(
    +                event instanceof InputEvent,
    +                `"input" event should be dispatched with InputEvent interface on ${element.tagName}`
    +              );
    +              is(
    +                event.inputType,
    +                "insertReplacementText",
    +                'inputType value should be "insertReplacementText"'
    +              );
    +              is(event.data, String(value), `data value should be "${value}"`);
    +              is(event.dataTransfer, null, "dataTransfer should be null");
    +              is(
    +                event.getTargetRanges().length,
    +                0,
    +                "getTargetRanges() should return empty array"
    +              );
    +            } else {
    +              ok(
    +                !beforeInputFired,
    +                `"beforeinput" event shouldn't be fired on ${element.tagName}`
    +              );
    +              ok(
    +                event instanceof Event && !(event instanceof UIEvent),
    +                `"input" event should be dispatched with Event interface on ${element.tagName}`
    +              );
    +            }
    +            is(
    +              event.cancelable,
    +              false,
    +              `"input" event should be never cancelable on ${element.tagName}`
    +            );
    +            is(
    +              event.bubbles,
    +              true,
    +              `"input" event should always bubble on ${element.tagName}`
    +            );
    +            resolve();
    +          },
    +          { once: true }
    +        );
    +      }),
    +      new Promise(resolve =>
    +        element.addEventListener(expectingEvent, resolve, { once: true })
    +      ),
    +    ]).then(() => checkFieldValue(element, value));
    +
    +    promises.push(checkFieldAutofilled);
    +  }
    +  // Press Enter key and trigger form autofill.
    +  synthesizeKey("KEY_Enter");
    +
    +  return Promise.all(promises);
    +}
    +
    +async function onStorageChanged(type) {
    +  info(`expecting the storage changed: ${type}`);
    +  return new Promise(resolve => {
    +    formFillChromeScript.addMessageListener(
    +      "formautofill-storage-changed",
    +      function onChanged(data) {
    +        formFillChromeScript.removeMessageListener(
    +          "formautofill-storage-changed",
    +          onChanged
    +        );
    +        is(data.data, type, `Receive ${type} storage changed event`);
    +        resolve();
    +      }
    +    );
    +  });
    +}
    +
    +function checkMenuEntries(expectedValues, isFormAutofillResult = true) {
    +  let actualValues = getMenuEntries();
    +  // Expect one more item would appear at the bottom as the footer if the result is from form autofill.
    +  let expectedLength = isFormAutofillResult
    +    ? expectedValues.length + 1
    +    : expectedValues.length;
    +
    +  is(actualValues.length, expectedLength, " Checking length of expected menu");
    +  for (let i = 0; i < expectedValues.length; i++) {
    +    is(actualValues[i], expectedValues[i], " Checking menu entry #" + i);
    +  }
    +}
    +
    +function invokeAsyncChromeTask(message, payload = {}) {
    +  info(`expecting the chrome task finished: ${message}`);
    +  return formFillChromeScript.sendQuery(message, payload);
    +}
    +
    +async function addAddress(address) {
    +  await invokeAsyncChromeTask("FormAutofillTest:AddAddress", { address });
    +  await sleep();
    +}
    +
    +async function removeAddress(guid) {
    +  return invokeAsyncChromeTask("FormAutofillTest:RemoveAddress", { guid });
    +}
    +
    +async function updateAddress(guid, address) {
    +  return invokeAsyncChromeTask("FormAutofillTest:UpdateAddress", {
    +    address,
    +    guid,
    +  });
    +}
    +
    +async function checkAddresses(expectedAddresses) {
    +  return invokeAsyncChromeTask("FormAutofillTest:CheckAddresses", {
    +    expectedAddresses,
    +  });
    +}
    +
    +async function cleanUpAddresses() {
    +  return invokeAsyncChromeTask("FormAutofillTest:CleanUpAddresses");
    +}
    +
    +async function addCreditCard(creditcard) {
    +  await invokeAsyncChromeTask("FormAutofillTest:AddCreditCard", { creditcard });
    +  await sleep();
    +}
    +
    +async function removeCreditCard(guid) {
    +  return invokeAsyncChromeTask("FormAutofillTest:RemoveCreditCard", { guid });
    +}
    +
    +async function checkCreditCards(expectedCreditCards) {
    +  return invokeAsyncChromeTask("FormAutofillTest:CheckCreditCards", {
    +    expectedCreditCards,
    +  });
    +}
    +
    +async function cleanUpCreditCards() {
    +  return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards");
    +}
    +
    +async function cleanUpStorage() {
    +  await cleanUpAddresses();
    +  await cleanUpCreditCards();
    +}
    +
    +async function canTestOSKeyStoreLogin() {
    +  let { canTest } = await invokeAsyncChromeTask(
    +    "FormAutofillTest:CanTestOSKeyStoreLogin"
    +  );
    +  return canTest;
    +}
    +
    +async function waitForOSKeyStoreLogin(login = false) {
    +  await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
    +}
    +
    +function patchRecordCCNumber(record) {
    +  const number = record["cc-number"];
    +  const ccNumberFmt = {
    +    affix: "****",
    +    label: number.substr(-4),
    +  };
    +
    +  return Object.assign({}, record, { ccNumberFmt });
    +}
    +
    +// Utils for registerPopupShownListener(in satchel_common.js) that handles dropdown popup
    +// Please call "initPopupListener()" in your test and "await expectPopup()"
    +// if you want to wait for dropdown menu displayed.
    +function expectPopup() {
    +  info("expecting a popup");
    +  return new Promise(resolve => {
    +    expectingPopup = resolve;
    +  });
    +}
    +
    +function notExpectPopup(ms = 500) {
    +  info("not expecting a popup");
    +  return new Promise((resolve, reject) => {
    +    expectingPopup = reject.bind(this, "Unexpected Popup");
    +    // TODO: We don't have an event to notify no popup showing, so wait for 500
    +    // ms (in default) to predict any unexpected popup showing.
    +    setTimeout(resolve, ms);
    +  });
    +}
    +
    +function popupShownListener() {
    +  info("popup shown for test ");
    +  if (expectingPopup) {
    +    expectingPopup();
    +    expectingPopup = null;
    +  }
    +}
    +
    +function initPopupListener() {
    +  registerPopupShownListener(popupShownListener);
    +}
    +
    +async function triggerPopupAndHoverItem(fieldSelector, selectIndex) {
    +  await focusAndWaitForFieldsIdentified(fieldSelector);
    +  synthesizeKey("KEY_ArrowDown");
    +  await expectPopup();
    +  for (let i = 0; i <= selectIndex; i++) {
    +    synthesizeKey("KEY_ArrowDown");
    +  }
    +  await notifySelectedIndex(selectIndex);
    +}
    +
    +function formAutoFillCommonSetup() {
    +  // Remove the /creditCard path segement when referenced from the 'creditCard' subdirectory.
    +  let chromeURL = SimpleTest.getTestFileURL(
    +    "formautofill_parent_utils.js"
    +  ).replace(/\/creditCard/, "");
    +  formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
    +  formFillChromeScript.addMessageListener("onpopupshown", ({ results }) => {
    +    gLastAutoCompleteResults = results;
    +    if (gPopupShownListener) {
    +      gPopupShownListener({ results });
    +    }
    +  });
    +
    +  add_setup(async () => {
    +    info(`expecting the storage setup`);
    +    await formFillChromeScript.sendQuery("setup");
    +  });
    +
    +  SimpleTest.registerCleanupFunction(async () => {
    +    info(`expecting the storage cleanup`);
    +    await formFillChromeScript.sendQuery("cleanup");
    +
    +    formFillChromeScript.destroy();
    +    expectingPopup = null;
    +  });
    +
    +  document.addEventListener(
    +    "DOMContentLoaded",
    +    function () {
    +      defaultTextColor = window
    +        .getComputedStyle(document.querySelector("input"))
    +        .getPropertyValue("color");
    +
    +      // This is needed for test_formautofill_preview_highlight.html to work properly
    +      let disabledInput = document.querySelector(`input[disabled]`);
    +      if (disabledInput) {
    +        defaultDisabledTextColor = window
    +          .getComputedStyle(disabledInput)
    +          .getPropertyValue("color");
    +      }
    +    },
    +    { once: true }
    +  );
    +}
    +
    +/*
    + * Extremely over-simplified detection of card type from card number just for
    + * our tests. This is needed to test the aria-label of credit card menu entries.
    + */
    +function getCCTypeName(creditCard) {
    +  return creditCard["cc-number"][0] == "4" ? "Visa" : "MasterCard";
    +}
    +
    +formAutoFillCommonSetup();
    diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
    new file mode 100644
    index 0000000000..0d9af772b5
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
    @@ -0,0 +1,304 @@
    +/* eslint-env mozilla/chrome-script */
    +
    +"use strict";
    +
    +const { FormAutofill } = ChromeUtils.importESModule(
    +  "resource://autofill/FormAutofill.sys.mjs"
    +);
    +const { FormAutofillUtils } = ChromeUtils.importESModule(
    +  "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
    +);
    +const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
    +  "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
    +);
    +
    +let { formAutofillStorage } = ChromeUtils.importESModule(
    +  "resource://autofill/FormAutofillStorage.sys.mjs"
    +);
    +
    +const { ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME } =
    +  FormAutofillUtils;
    +
    +let destroyed = false;
    +
    +var ParentUtils = {
    +  getFormAutofillActor() {
    +    let win = Services.wm.getMostRecentWindow("navigator:browser");
    +    let selectedBrowser = win.gBrowser.selectedBrowser;
    +    return selectedBrowser.browsingContext.currentWindowGlobal.getActor(
    +      "FormAutofill"
    +    );
    +  },
    +
    +  _getRecords(collectionName) {
    +    return this.getFormAutofillActor().receiveMessage({
    +      name: "FormAutofill:GetRecords",
    +      data: {
    +        searchString: "",
    +        collectionName,
    +      },
    +    });
    +  },
    +
    +  async _storageChangeObserved({
    +    topic = "formautofill-storage-changed",
    +    type,
    +    times = 1,
    +  }) {
    +    let count = times;
    +
    +    return new Promise(resolve => {
    +      Services.obs.addObserver(function observer(subject, obsTopic, data) {
    +        if ((type && data != type) || !!--count) {
    +          return;
    +        }
    +
    +        // every notification type should have the collection name.
    +        // We're not allowed to trigger assertions during mochitest
    +        // cleanup functions.
    +        if (!destroyed) {
    +          let allowedNames = [
    +            ADDRESSES_COLLECTION_NAME,
    +            CREDITCARDS_COLLECTION_NAME,
    +          ];
    +          assert.ok(
    +            allowedNames.includes(subject.wrappedJSObject.collectionName),
    +            "should include the collection name"
    +          );
    +          // every notification except removeAll should have a guid.
    +          if (data != "removeAll") {
    +            assert.ok(subject.wrappedJSObject.guid, "should have a guid");
    +          }
    +        }
    +        Services.obs.removeObserver(observer, obsTopic);
    +        resolve();
    +      }, topic);
    +    });
    +  },
    +
    +  async _operateRecord(collectionName, type, msgData) {
    +    let msgName, times, topic;
    +
    +    if (collectionName == ADDRESSES_COLLECTION_NAME) {
    +      switch (type) {
    +        case "add": {
    +          msgName = "FormAutofill:SaveAddress";
    +          break;
    +        }
    +        case "update": {
    +          msgName = "FormAutofill:SaveAddress";
    +          break;
    +        }
    +        case "remove": {
    +          msgName = "FormAutofill:RemoveAddresses";
    +          times = msgData.guids.length;
    +          break;
    +        }
    +        default:
    +          return;
    +      }
    +    } else {
    +      switch (type) {
    +        case "add": {
    +          msgData = Object.assign({}, msgData);
    +          msgName = "FormAutofill:SaveCreditCard";
    +          break;
    +        }
    +        case "remove": {
    +          msgName = "FormAutofill:RemoveCreditCards";
    +          times = msgData.guids.length;
    +          break;
    +        }
    +        default:
    +          return;
    +      }
    +    }
    +
    +    let storageChangePromise = this._storageChangeObserved({
    +      type,
    +      times,
    +      topic,
    +    });
    +    this.getFormAutofillActor().receiveMessage({
    +      name: msgName,
    +      data: msgData,
    +    });
    +    await storageChangePromise;
    +  },
    +
    +  async operateAddress(type, msgData) {
    +    await this._operateRecord(ADDRESSES_COLLECTION_NAME, ...arguments);
    +  },
    +
    +  async operateCreditCard(type, msgData) {
    +    await this._operateRecord(CREDITCARDS_COLLECTION_NAME, ...arguments);
    +  },
    +
    +  async cleanUpAddresses() {
    +    const guids = (await this._getRecords(ADDRESSES_COLLECTION_NAME)).map(
    +      record => record.guid
    +    );
    +
    +    if (!guids.length) {
    +      return;
    +    }
    +
    +    await this.operateAddress(
    +      "remove",
    +      { guids },
    +      "FormAutofillTest:AddressesCleanedUp"
    +    );
    +  },
    +
    +  async cleanUpCreditCards() {
    +    if (!FormAutofill.isAutofillCreditCardsAvailable) {
    +      return;
    +    }
    +    const guids = (await this._getRecords(CREDITCARDS_COLLECTION_NAME)).map(
    +      record => record.guid
    +    );
    +
    +    if (!guids.length) {
    +      return;
    +    }
    +
    +    await this.operateCreditCard(
    +      "remove",
    +      { guids },
    +      "FormAutofillTest:CreditCardsCleanedUp"
    +    );
    +  },
    +
    +  setup() {
    +    OSKeyStoreTestUtils.setup();
    +  },
    +
    +  async cleanup() {
    +    await this.cleanUpAddresses();
    +    await this.cleanUpCreditCards();
    +    await OSKeyStoreTestUtils.cleanup();
    +
    +    Services.obs.removeObserver(this, "formautofill-storage-changed");
    +  },
    +
    +  _areRecordsMatching(recordA, recordB, collectionName) {
    +    for (let field of formAutofillStorage[collectionName].VALID_FIELDS) {
    +      if (recordA[field] !== recordB[field]) {
    +        return false;
    +      }
    +    }
    +    // Check the internal field if both addresses have valid value.
    +    for (let field of formAutofillStorage.INTERNAL_FIELDS) {
    +      if (
    +        field in recordA &&
    +        field in recordB &&
    +        recordA[field] !== recordB[field]
    +      ) {
    +        return false;
    +      }
    +    }
    +    return true;
    +  },
    +
    +  async _checkRecords(collectionName, expectedRecords) {
    +    const records = await this._getRecords(collectionName);
    +
    +    if (records.length !== expectedRecords.length) {
    +      return false;
    +    }
    +
    +    for (let record of records) {
    +      let matching = expectedRecords.some(expectedRecord => {
    +        return ParentUtils._areRecordsMatching(
    +          record,
    +          expectedRecord,
    +          collectionName
    +        );
    +      });
    +
    +      if (!matching) {
    +        return false;
    +      }
    +    }
    +
    +    return true;
    +  },
    +
    +  async checkAddresses({ expectedAddresses }) {
    +    return this._checkRecords(ADDRESSES_COLLECTION_NAME, expectedAddresses);
    +  },
    +
    +  async checkCreditCards({ expectedCreditCards }) {
    +    return this._checkRecords(CREDITCARDS_COLLECTION_NAME, expectedCreditCards);
    +  },
    +
    +  observe(subject, topic, data) {
    +    if (!destroyed) {
    +      assert.ok(topic === "formautofill-storage-changed");
    +    }
    +    sendAsyncMessage("formautofill-storage-changed", {
    +      subject: null,
    +      topic,
    +      data,
    +    });
    +  },
    +};
    +
    +Services.obs.addObserver(ParentUtils, "formautofill-storage-changed");
    +
    +Services.mm.addMessageListener("FormAutofill:FieldsIdentified", () => {
    +  return null;
    +});
    +
    +addMessageListener("FormAutofillTest:AddAddress", msg => {
    +  return ParentUtils.operateAddress("add", msg);
    +});
    +
    +addMessageListener("FormAutofillTest:RemoveAddress", msg => {
    +  return ParentUtils.operateAddress("remove", msg);
    +});
    +
    +addMessageListener("FormAutofillTest:UpdateAddress", msg => {
    +  return ParentUtils.operateAddress("update", msg);
    +});
    +
    +addMessageListener("FormAutofillTest:CheckAddresses", msg => {
    +  return ParentUtils.checkAddresses(msg);
    +});
    +
    +addMessageListener("FormAutofillTest:CleanUpAddresses", msg => {
    +  return ParentUtils.cleanUpAddresses();
    +});
    +
    +addMessageListener("FormAutofillTest:AddCreditCard", msg => {
    +  return ParentUtils.operateCreditCard("add", msg);
    +});
    +
    +addMessageListener("FormAutofillTest:RemoveCreditCard", msg => {
    +  return ParentUtils.operateCreditCard("remove", msg);
    +});
    +
    +addMessageListener("FormAutofillTest:CheckCreditCards", msg => {
    +  return ParentUtils.checkCreditCards(msg);
    +});
    +
    +addMessageListener("FormAutofillTest:CleanUpCreditCards", msg => {
    +  return ParentUtils.cleanUpCreditCards();
    +});
    +
    +addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", msg => {
    +  return { canTest: OSKeyStoreTestUtils.canTestOSKeyStoreLogin() };
    +});
    +
    +addMessageListener("FormAutofillTest:OSKeyStoreLogin", async msg => {
    +  await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
    +});
    +
    +addMessageListener("setup", async () => {
    +  ParentUtils.setup();
    +});
    +
    +addMessageListener("cleanup", async () => {
    +  destroyed = true;
    +  await ParentUtils.cleanup();
    +});
    diff --git a/browser/extensions/formautofill/test/mochitest/mochitest.ini b/browser/extensions/formautofill/test/mochitest/mochitest.ini
    new file mode 100644
    index 0000000000..2964fd5b13
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/mochitest.ini
    @@ -0,0 +1,23 @@
    +[DEFAULT]
    +prefs =
    +  extensions.formautofill.creditCards.supported=on
    +  extensions.formautofill.creditCards.enabled=true
    +  extensions.formautofill.addresses.supported=on
    +  extensions.formautofill.addresses.enabled=true
    +skip-if = toolkit == 'android' # bug 1730213
    +support-files =
    +  ../../../../../toolkit/components/satchel/test/satchel_common.js
    +  ../../../../../toolkit/components/satchel/test/parent_utils.js
    +  formautofill_common.js
    +  formautofill_parent_utils.js
    +
    +[test_address_level_1_submission.html]
    +[test_autofill_and_ordinal_forms.html]
    +[test_autofocus_form.html]
    +[test_basic_autocomplete_form.html]
    +[test_form_changes.html]
    +[test_formautofill_preview_highlight.html]
    +skip-if = verify
    +[test_multi_locale_CA_address_form.html]
    +[test_multiple_forms.html]
    +[test_on_address_submission.html]
    diff --git a/browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html b/browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html
    new file mode 100644
    index 0000000000..9d6ad1f7ea
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html
    @@ -0,0 +1,102 @@
    +
    +
    +
    +  
    +  Test autofill submission for a country without address-level1
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: Test autofill submission for a country without address-level1
    +
    +
    +
    +
    + + +
    +

    +

    +

    +

    +

    +
    + +
    + + diff --git a/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html new file mode 100644 index 0000000000..5c143c3f4a --- /dev/null +++ b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html @@ -0,0 +1,116 @@ + + + + + Test autofill submit + + + + + + + + + + +
    + +

    Address form

    +
    + + + + + + + + + + +

    + + +

    +
    + +

    Ordinal form

    +
    + +

    +
    + +
    + + diff --git a/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html b/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html new file mode 100644 index 0000000000..e2240474c8 --- /dev/null +++ b/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html @@ -0,0 +1,69 @@ + + + + + Test basic autofill + + + + + + + +Form autofill test: autocomplete on an autofocus form + + + +

    + +
    + +
    +

    This is a basic form.

    +

    + +

    +

    +

    +
    + +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
    new file mode 100644
    index 0000000000..a642b2abca
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
    @@ -0,0 +1,220 @@
    +
    +
    +
    +  
    +  Test basic autofill
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: simple form address autofill
    +
    +
    +
    +

    + +
    + +
    +

    This is a basic form.

    +

    +

    +

    +

    +

    +

    +

    +
    + +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/test_form_changes.html b/browser/extensions/formautofill/test/mochitest/test_form_changes.html
    new file mode 100644
    index 0000000000..2eace91a53
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/test_form_changes.html
    @@ -0,0 +1,128 @@
    +
    +
    +
    +  
    +  Test basic autofill
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: autocomplete on an autofocus form
    +
    +
    +
    +

    +
    +
    +

    +

    +

    +
    +
    +

    +

    +

    +
    +
    +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
    new file mode 100644
    index 0000000000..b32b036c9c
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
    @@ -0,0 +1,121 @@
    +
    +
    +
    +  
    +  Test form autofill - preview and highlight
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: preview and highlight
    +
    +
    +
    +

    + +
    + +
    +

    This is a basic form.

    +

    +

    +

    +

    +

    +

    +

    +
    + +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html b/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
    new file mode 100644
    index 0000000000..48e0caa785
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
    @@ -0,0 +1,273 @@
    +
    +
    +
    +  
    +  Test basic autofill
    +  
    +  
    +  
    +  
    +  
    +
    +
    +Form autofill test: simple form address autofill
    +
    +
    +
    +

    + +
    + +
    +

    This is a basic CA form with en address level 1 select.

    +

    +

    +

    +

    +

    +

    +

    +
    + +
    +

    This is a basic CA form with fr address level 1 select.

    +

    +

    +

    +

    +

    +

    +

    +
    + +
    + +
    
    +
    +
    diff --git a/browser/extensions/formautofill/test/mochitest/test_multiple_forms.html b/browser/extensions/formautofill/test/mochitest/test_multiple_forms.html
    new file mode 100644
    index 0000000000..feea55aae6
    --- /dev/null
    +++ b/browser/extensions/formautofill/test/mochitest/test_multiple_forms.html
    @@ -0,0 +1,67 @@
    +
    +
    +
    +  
    +  Test autofill submit
    +  
    +  
    +  
    +  
    +  
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    + +
    + + + +
    + +
    + + diff --git a/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html b/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html new file mode 100644 index 0000000000..79da48c77b --- /dev/null +++ b/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html @@ -0,0 +1,121 @@ + + + + + Test autofill submit + + + + + + + +Form autofill test: check if address is saved/updated correctly + + + +
    + +
    +

    This is a basic form for submitting test.

    +

    +

    +

    +

    +

    +
    + +
    + + diff --git a/browser/extensions/formautofill/test/unit/head.js b/browser/extensions/formautofill/test/unit/head.js new file mode 100644 index 0000000000..e5353833ef --- /dev/null +++ b/browser/extensions/formautofill/test/unit/head.js @@ -0,0 +1,357 @@ +/** + * Provides infrastructure for automated formautofill components tests. + */ + +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { ObjectUtils } = ChromeUtils.import( + "resource://gre/modules/ObjectUtils.jsm" +); +var { FormLikeFactory } = ChromeUtils.importESModule( + "resource://gre/modules/FormLikeFactory.sys.mjs" +); +var { FormAutofillHandler } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHandler.sys.mjs" +); +var { AddonTestUtils, MockAsyncShutdown } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +var { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); +var { FileTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/FileTestUtils.sys.mjs" +); +var { MockDocument } = ChromeUtils.importESModule( + "resource://testing-common/MockDocument.sys.mjs" +); +var { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs", + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +{ + // We're going to register a mock file source + // with region names based on en-US. This is + // necessary for tests that expect to match + // on region code display names. + const fs = [ + { + path: "toolkit/intl/regionNames.ftl", + source: ` +region-name-us = United States +region-name-nz = New Zealand +region-name-au = Australia +region-name-ca = Canada +region-name-tw = Taiwan + `, + }, + ]; + + let locales = Services.locale.packagedLocales; + const mockSource = L10nFileSource.createMock( + "mock", + "app", + locales, + "resource://mock_path", + fs + ); + L10nRegistry.getInstance().registerSources([mockSource]); +} + +do_get_profile(); + +const EXTENSION_ID = "formautofill@mozilla.org"; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +function SetPref(name, value) { + switch (typeof value) { + case "string": + Services.prefs.setCharPref(name, value); + break; + case "number": + Services.prefs.setIntPref(name, value); + break; + case "boolean": + Services.prefs.setBoolPref(name, value); + break; + default: + throw new Error("Unknown type"); + } +} + +// Return the current date rounded in the manner that sync does. +function getDateForSync() { + return Math.round(Date.now() / 10) / 100; +} + +async function loadExtension() { + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1.9.2" + ); + await AddonTestUtils.promiseStartupManager(); + + let extensionPath = Services.dirsvc.get("GreD", Ci.nsIFile); + extensionPath.append("browser"); + extensionPath.append("features"); + extensionPath.append(EXTENSION_ID); + + if (!extensionPath.exists()) { + extensionPath.leafName = `${EXTENSION_ID}.xpi`; + } + + let startupPromise = new Promise(resolve => { + const { apiManager } = ExtensionParent; + function onReady(event, extension) { + if (extension.id == EXTENSION_ID) { + apiManager.off("ready", onReady); + resolve(); + } + } + + apiManager.on("ready", onReady); + }); + + await AddonManager.installTemporaryAddon(extensionPath); + await startupPromise; +} + +// Returns a reference to a temporary file that is guaranteed not to exist and +// is cleaned up later. See FileTestUtils.getTempFile for details. +function getTempFile(leafName) { + return FileTestUtils.getTempFile(leafName); +} + +async function initProfileStorage( + fileName, + records, + collectionName = "addresses" +) { + let { FormAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + ); + let path = getTempFile(fileName).path; + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + // AddonTestUtils inserts its own directory provider that manages TmpD. + // It removes that directory at shutdown, which races with shutdown + // handing in JSONFile/DeferredTask (which is used by FormAutofillStorage). + // Avoid the race by explicitly finalizing any formautofill JSONFile + // instances created manually by individual tests when the test finishes. + registerCleanupFunction(function finalizeAutofillStorage() { + return profileStorage._finalize(); + }); + + if (!records || !Array.isArray(records)) { + return profileStorage; + } + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "add" && subject.wrappedJSObject.collectionName == collectionName + ); + for (let record of records) { + Assert.ok(await profileStorage[collectionName].add(record)); + await onChanged; + } + await profileStorage._saveImmediately(); + return profileStorage; +} + +function verifySectionAutofillResult(sections, expectedSectionsInfo) { + sections.forEach((section, index) => { + const expectedSection = expectedSectionsInfo[index]; + + const fieldDetails = section.fieldDetails; + const expectedFieldDetails = expectedSection.fields; + + info(`verify autofill section[${index}]`); + + fieldDetails.forEach((field, fieldIndex) => { + const expeceted = expectedFieldDetails[fieldIndex]; + + Assert.equal( + expeceted.autofill, + field.element.value, + `Autofilled value for element(id=${field.element.id}, field name=${field.fieldName}) should be equal` + ); + }); + }); +} + +function verifySectionFieldDetails(sections, expectedSectionsInfo) { + sections.forEach((section, index) => { + const expectedSection = expectedSectionsInfo[index]; + + const fieldDetails = section.fieldDetails; + const expectedFieldDetails = expectedSection.fields; + + info(`section[${index}] ${expectedSection.description ?? ""}:`); + info(`FieldName Prediction Results: ${fieldDetails.map(i => i.fieldName)}`); + info( + `FieldName Expected Results: ${expectedFieldDetails.map( + detail => detail.fieldName + )}` + ); + Assert.equal( + fieldDetails.length, + expectedFieldDetails.length, + `Expected field count.` + ); + + fieldDetails.forEach((field, fieldIndex) => { + const expectedFieldDetail = expectedFieldDetails[fieldIndex]; + + const expected = { + ...{ + reason: "autocomplete", + section: "", + contactType: "", + addressType: "", + }, + ...expectedSection.default, + ...expectedFieldDetail, + }; + + const keys = new Set([...Object.keys(field), ...Object.keys(expected)]); + ["autofill", "elementWeakRef", "confidence", "part"].forEach(k => + keys.delete(k) + ); + + for (const key of keys) { + const expectedValue = expected[key]; + const actualValue = field[key]; + Assert.equal( + expectedValue, + actualValue, + `${key} should be equal, expect ${expectedValue}, got ${actualValue}` + ); + } + }); + + Assert.equal( + section.isValidSection(), + !expectedSection.invalid, + `Should be an ${expectedSection.invalid ? "invalid" : "valid"} section` + ); + }); +} + +var FormAutofillHeuristics, LabelUtils; +var AddressDataLoader, FormAutofillUtils; + +function autofillFieldSelector(doc) { + return doc.querySelectorAll("input, select"); +} + +/** + * Returns the Sync change counter for a profile storage record. Synced records + * store additional metadata for tracking changes and resolving merge conflicts. + * Deleting a synced record replaces the record with a tombstone. + * + * @param {AutofillRecords} records + * The `AutofillRecords` instance to query. + * @param {string} guid + * The GUID of the record or tombstone. + * @returns {number} + * The change counter, or -1 if the record doesn't exist or hasn't + * been synced yet. + */ +function getSyncChangeCounter(records, guid) { + let record = records._findByGUID(guid, { includeDeleted: true }); + if (!record) { + return -1; + } + let sync = records._getSyncMetaData(record); + if (!sync) { + return -1; + } + return sync.changeCounter; +} + +/** + * Performs a partial deep equality check to determine if an object contains + * the given fields. + * + * @param {object} object + * The object to check. Unlike `ObjectUtils.deepEqual`, properties in + * `object` that are not in `fields` will be ignored. + * @param {object} fields + * The fields to match. + * @returns {boolean} + * Does `object` contain `fields` with matching values? + */ +function objectMatches(object, fields) { + let actual = {}; + for (let key in fields) { + if (!object.hasOwnProperty(key)) { + return false; + } + actual[key] = object[key]; + } + return ObjectUtils.deepEqual(actual, fields); +} + +add_setup(async function head_initialize() { + Services.prefs.setBoolPref("extensions.experiments.enabled", true); + Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true); + + Services.prefs.setCharPref( + "extensions.formautofill.addresses.supported", + "on" + ); + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supported", + "on" + ); + Services.prefs.setBoolPref("extensions.formautofill.addresses.enabled", true); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + true + ); + + // Clean up after every test. + registerCleanupFunction(function head_cleanup() { + Services.prefs.clearUserPref("extensions.experiments.enabled"); + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.supported" + ); + Services.prefs.clearUserPref("extensions.formautofill.addresses.supported"); + Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); + Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill"); + Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); + Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); + }); + + await loadExtension(); +}); + +let OSKeyStoreTestUtils; +add_setup(async function os_key_store_setup() { + ({ OSKeyStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/OSKeyStoreTestUtils.sys.mjs" + )); + OSKeyStoreTestUtils.setup(); + registerCleanupFunction(async function cleanup() { + await OSKeyStoreTestUtils.cleanup(); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/head_addressComponent.js b/browser/extensions/formautofill/test/unit/head_addressComponent.js new file mode 100644 index 0000000000..472e4ee589 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/head_addressComponent.js @@ -0,0 +1,69 @@ +"use strict"; + +/* exported BOTH_EMPTY, A_IS_EMPTY, B_IS_EMPTY, A_CONTAINS_B, B_CONTAINS_A, SIMILAR, SAME, DIFFERENT, runIsValidTest, runCompareTest */ + +const { AddressComparison, AddressComponent } = ChromeUtils.importESModule( + "resource://gre/modules/shared/AddressComponent.sys.mjs" +); + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +const BOTH_EMPTY = AddressComparison.BOTH_EMPTY; +const A_IS_EMPTY = AddressComparison.A_IS_EMPTY; +const B_IS_EMPTY = AddressComparison.B_IS_EMPTY; +const A_CONTAINS_B = AddressComparison.A_CONTAINS_B; +const B_CONTAINS_A = AddressComparison.B_CONTAINS_A; +const SIMILAR = AddressComparison.SIMILAR; +const SAME = AddressComparison.SAME; +const DIFFERENT = AddressComparison.DIFFERENT; + +function runIsValidTest(tests, fieldName, funcSetupRecord) { + let region = FormAutofill.DEFAULT_REGION; + for (const test of tests) { + if (!Array.isArray(test)) { + region = test.region; + info(`Change region to ${JSON.stringify(test.region)}`); + continue; + } + + const [testValue, expected] = test; + const record = funcSetupRecord(testValue); + + const field = new AddressComponent(record, region).getField(fieldName); + const result = field.isValid(); + Assert.equal( + result, + expected, + `Expect isValid returns ${expected} for ${testValue}` + ); + } +} + +function runCompareTest(tests, fieldName, funcSetupRecord) { + let region = FormAutofill.DEFAULT_REGION; + for (const test of tests) { + if (!Array.isArray(test)) { + info(`change region to ${JSON.stringify(test.region)}`); + region = test.region; + continue; + } + + const [v1, v2, expected] = test; + const r1 = funcSetupRecord(v1); + const f1 = new AddressComponent(r1, region).getField(fieldName); + + const r2 = funcSetupRecord(v2); + const f2 = new AddressComponent(r2, region).getField(fieldName); + + const result = AddressComparison.compare(f1, f2); + const resultString = AddressComparison.resultToString(result); + const expectedString = AddressComparison.resultToString(expected); + Assert.equal( + result, + expected, + `Expect ${expectedString} when comparing "${v1}" & "${v2}", got ${resultString}` + ); + } +} diff --git a/browser/extensions/formautofill/test/unit/test_activeStatus.js b/browser/extensions/formautofill/test/unit/test_activeStatus.js new file mode 100644 index 0000000000..47d79b02e5 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_activeStatus.js @@ -0,0 +1,176 @@ +/* + * Test for status handling in Form Autofill Parent. + */ + +"use strict"; + +let FormAutofillStatus; + +add_setup(async () => { + ({ FormAutofillStatus } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillParent.sys.mjs" + )); +}); + +add_task(async function test_activeStatus_init() { + sinon.spy(FormAutofillStatus, "updateStatus"); + + // Default status is null before initialization + Assert.equal(FormAutofillStatus._active, null); + Assert.equal(Services.ppmm.sharedData.get("FormAutofill:enabled"), undefined); + + FormAutofillStatus.init(); + // init shouldn't call updateStatus since that requires storage which will + // lead to startup time regressions. + Assert.equal(FormAutofillStatus.updateStatus.called, false); + Assert.equal(Services.ppmm.sharedData.get("FormAutofill:enabled"), undefined); + + // Initialize profile storage + await FormAutofillStatus.formAutofillStorage.initialize(); + await FormAutofillStatus.updateSavedFieldNames(); + // Upon first initializing profile storage, status should be computed. + Assert.equal(FormAutofillStatus.updateStatus.called, true); + Assert.equal(Services.ppmm.sharedData.get("FormAutofill:enabled"), false); + + FormAutofillStatus.uninit(); +}); + +add_task(async function test_activeStatus_observe() { + FormAutofillStatus.init(); + sinon.stub(FormAutofillStatus, "computeStatus"); + sinon.spy(FormAutofillStatus, "onStatusChanged"); + + // _active = _computeStatus() => No need to trigger _onStatusChanged + FormAutofillStatus._active = true; + FormAutofillStatus.computeStatus.returns(true); + FormAutofillStatus.observe( + null, + "nsPref:changed", + "extensions.formautofill.addresses.enabled" + ); + FormAutofillStatus.observe( + null, + "nsPref:changed", + "extensions.formautofill.creditCards.enabled" + ); + Assert.equal(FormAutofillStatus.onStatusChanged.called, false); + + // _active != computeStatus() => Need to trigger onStatusChanged + FormAutofillStatus.computeStatus.returns(false); + FormAutofillStatus.onStatusChanged.resetHistory(); + FormAutofillStatus.observe( + null, + "nsPref:changed", + "extensions.formautofill.addresses.enabled" + ); + FormAutofillStatus.observe( + null, + "nsPref:changed", + "extensions.formautofill.creditCards.enabled" + ); + Assert.equal(FormAutofillStatus.onStatusChanged.called, true); + + // profile changed => Need to trigger _onStatusChanged + await Promise.all( + ["add", "update", "remove", "reconcile"].map(async event => { + FormAutofillStatus.computeStatus.returns(!FormAutofillStatus._active); + FormAutofillStatus.onStatusChanged.resetHistory(); + await FormAutofillStatus.observe( + null, + "formautofill-storage-changed", + event + ); + Assert.equal(FormAutofillStatus.onStatusChanged.called, true); + }) + ); + + // profile metadata updated => No need to trigger onStatusChanged + FormAutofillStatus.computeStatus.returns(!FormAutofillStatus._active); + FormAutofillStatus.onStatusChanged.resetHistory(); + await FormAutofillStatus.observe( + null, + "formautofill-storage-changed", + "notifyUsed" + ); + Assert.equal(FormAutofillStatus.onStatusChanged.called, false); + + FormAutofillStatus.computeStatus.restore(); +}); + +add_task(async function test_activeStatus_computeStatus() { + registerCleanupFunction(function cleanup() { + Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); + Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); + }); + + sinon.stub( + FormAutofillStatus.formAutofillStorage.addresses, + "getSavedFieldNames" + ); + FormAutofillStatus.formAutofillStorage.addresses.getSavedFieldNames.returns( + Promise.resolve(new Set()) + ); + sinon.stub( + FormAutofillStatus.formAutofillStorage.creditCards, + "getSavedFieldNames" + ); + FormAutofillStatus.formAutofillStorage.creditCards.getSavedFieldNames.returns( + Promise.resolve(new Set()) + ); + + // pref is enabled and profile is empty. + Services.prefs.setBoolPref("extensions.formautofill.addresses.enabled", true); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + true + ); + Assert.equal(FormAutofillStatus.computeStatus(), false); + + // pref is disabled and profile is empty. + Services.prefs.setBoolPref( + "extensions.formautofill.addresses.enabled", + false + ); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + false + ); + Assert.equal(FormAutofillStatus.computeStatus(), false); + + FormAutofillStatus.formAutofillStorage.addresses.getSavedFieldNames.returns( + Promise.resolve(new Set(["given-name"])) + ); + await FormAutofillStatus.observe(null, "formautofill-storage-changed", "add"); + + // pref is enabled and profile is not empty. + Services.prefs.setBoolPref("extensions.formautofill.addresses.enabled", true); + Assert.equal(FormAutofillStatus.computeStatus(), true); + + // pref is partial enabled and profile is not empty. + Services.prefs.setBoolPref("extensions.formautofill.addresses.enabled", true); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + false + ); + Assert.equal(FormAutofillStatus.computeStatus(), true); + Services.prefs.setBoolPref( + "extensions.formautofill.addresses.enabled", + false + ); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + true + ); + Assert.equal(FormAutofillStatus.computeStatus(), true); + + // pref is disabled and profile is not empty. + Services.prefs.setBoolPref( + "extensions.formautofill.addresses.enabled", + false + ); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + false + ); + Assert.equal(FormAutofillStatus.computeStatus(), false); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_city.js b/browser/extensions/formautofill/test/unit/test_addressComponent_city.js new file mode 100644 index 0000000000..8bb448cfe4 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_city.js @@ -0,0 +1,27 @@ +"use strict"; + +const VALID_TESTS = [["New York City", true]]; + +const COMPARE_TESTS = [ + ["New York City", "New York City", SAME], + ["New York City", "new york city", SAME], + ["New York City", "New York City", SIMILAR], // Merge whitespace + ["Happy Valley-Goose Bay", "Happy Valley Goose Bay", SIMILAR], // Replace punctuation with whitespace + ["New York City", "New York", A_CONTAINS_B], + ["New York", "NewYork", DIFFERENT], + ["New York City", "City New York", DIFFERENT], +]; + +const TEST_FIELD_NAME = "City"; + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { "address-level2": value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { "address-level2": value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_country.js b/browser/extensions/formautofill/test/unit/test_addressComponent_country.js new file mode 100644 index 0000000000..bf73309c60 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_country.js @@ -0,0 +1,47 @@ +"use strict"; + +const VALID_TESTS = [ + ["United States", true], + ["Not United States", true], // Invalid country name will be replaced with the default region, so + // it is still valid +]; + +const COMPARE_TESTS = [ + // United Stats, US, USA, America, U.S.A. + { region: "US" }, + ["United States", "United States", SAME], + ["United States", "united states", SAME], + ["United States", "US", SAME], + ["America", "United States", SAME], + ["America", "US", SAME], + ["US", "USA", SAME], + ["United States", "U.S.A.", SAME], // Normalize + + ["USB", "US", SAME], + + // Canada, Can, CA + ["CA", "Canada", SAME], + ["CA", "CAN", SAME], + ["CA", "US", DIFFERENT], + + { region: "DE" }, + ["USB", "US", DIFFERENT], + ["United States", "Germany", DIFFERENT], + + ["Invalid Country Name", "Germany", SAME], + ["AAA", "BBB", SAME], +]; + +const TEST_FIELD_NAME = "Country"; + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { country: value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { country: value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_email.js b/browser/extensions/formautofill/test/unit/test_addressComponent_email.js new file mode 100644 index 0000000000..2c4b07a542 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_email.js @@ -0,0 +1,74 @@ +"use strict"; + +// TODO: +// https://help.xmatters.com/ondemand/trial/valid_email_format.htm what is the allow characters??? +const VALID_TESTS = [ + // Is Valid Test + // [email1, expected] + ["john.doe@mozilla.org", true], + + ["", false], // empty + ["@mozilla.org", false], // without username + ["john.doe@", false], // without domain + ["john.doe@-mozilla.org", false], // domain starts with '-' + ["john.doe@mozilla.org-", false], // domain ends with '-' + ["john.doe@mozilla.-com.au", false], // sub-domain starts with '-' + ["john.doe@mozilla.com-.au", false], // sub-domain ends with '-' + + ["john-doe@mozilla.org", true], // dash (ok) + + // Special characters check + ["john.!#$%&'*+-/=?^_`{|}~doe@gmail.com", true], + + ["john.doe@work@mozilla.org", false], + ["äbc@mail.com", false], + + ["john.doe@" + "a".repeat(63) + ".org", true], + ["john.doe@" + "a".repeat(64) + ".org", false], + + // The following are commented out since we're using a more relax email + // validation algorithm now. + /* + ["-john.doe@mozilla.org", false], // username starts with '-' + [".john.doe@mozilla.org", false], // username starts with '.' + ["john.doe-@mozilla.org", true], // username ends with '-' ??? + ["john.doe.@mozilla.org", true], // username ends with '.' ??? + ["john.doe@-mozilla.org", true], // domain starts with '.' ??? + ["john..doe@mozilla.org", false], // consecutive period + ["john.-doe@mozilla.org", false], // period + dash + ["john-.doe@mozilla.org", false], // dash + period + ["john.doe@school.123", false], + ["peter-parker@spiderman", false], + + ["a".repeat(64) + "@mydomain.com", true], // length of username + ["b".repeat(65) + "@mydomain.com", false], +*/ +]; + +const COMPARE_TESTS = [ + // Same + ["test@mozilla.org", "test@mozilla.org", SAME], + ["john.doe@example.com", "jOhn.doE@example.com", SAME], + ["jan@gmail.com", "JAN@gmail.com", SAME], + + // Different + ["jan@gmail.com", "jan1@gmail.com", DIFFERENT], + ["jan@gmail.com", "jan@gmail.com.au", DIFFERENT], + ["john#smith@gmail.com", "johnsmith@gmail.com", DIFFERENT], +]; + +const TEST_FIELD_NAME = "Email"; + +add_setup(async () => {}); + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { email: value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { email: value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_name.js b/browser/extensions/formautofill/test/unit/test_addressComponent_name.js new file mode 100644 index 0000000000..79fb1879d2 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_name.js @@ -0,0 +1,101 @@ +"use strict"; + +const VALID_TESTS = [ + ["John Doe", true], + ["John O'Brian'", true], + ["John O-Brian'", true], + ["John Doe", true], +]; + +// prettier-ignore +const COMPARE_TESTS = [ + // Same + ["John", "John", SAME], // first name + ["John Doe", "John Doe", SAME], // first and last name + ["John Middle Doe", "John Middle Doe", SAME], // first, middle, and last name + ["John Mid1 Mid2 Doe", "John Mid1 Mid2 Doe", SAME], + + // Same: case insenstive + ["John Doe", "john doe", SAME], + + // Similar: whitespaces are merged + ["John Doe", "John Doe", SIMILAR], + + // Similar: asscent and base + ["John Doe", "John Döe", SIMILAR], // asscent and base + + // A Contains B + ["John Doe", "Doe", A_CONTAINS_B], // first + family name contains family name + ["John Doe", "John", A_CONTAINS_B], // first + family name contains first name + ["John Middle Doe", "Doe", A_CONTAINS_B], // [first, middle, last] contains [last] + ["John Middle Doe", "John", A_CONTAINS_B], // [first, middle, last] contains [first] + ["John Middle Doe", "Middle", A_CONTAINS_B], // [first, middle, last] contains [middle] + ["John Middle Doe", "Middle Doe", A_CONTAINS_B], // [first, middle, last] contains [middle, last] + ["John Middle Doe", "John Middle", A_CONTAINS_B], // [first, middle, last] contains [fisrt, middle] + ["John Middle Doe", "John Doe", A_CONTAINS_B], // [first, middle, last] contains [fisrt, last] + ["John Mary Jane Doe", "John Doe", A_CONTAINS_B], // [first, middle, last] contains [fisrt, last] + + // Different + ["John Doe", "Jane Roe", DIFFERENT], + ["John Doe", "Doe John", DIFFERENT], // swap order + ["John Middle Doe", "Middle John", DIFFERENT], + ["John Middle Doe", "Doe Middle", DIFFERENT], + ["John Doe", "John Roe.", DIFFERENT], // different family name + ["John Doe", "Jane Doe", DIFFERENT], // different given name + ["John Middle Doe", "Jane Michael Doe", DIFFERENT], // different middle name + + // Puncuation is either removed or replaced with white space + ["John O'Brian", "John OBrian", SIMILAR], + ["John O'Brian", "John O-Brian", SIMILAR], + ["John O'Brian", "John O Brian", SIMILAR], + ["John-Mary Doe", "JohnMary Doe", SIMILAR], + ["John-Mary Doe", "John'Mary Doe", SIMILAR], + ["John-Mary Doe", "John Mary Doe", SIMILAR], + ["John-Mary Doe", "John Mary", A_CONTAINS_B], + + // Test Name Variants + ["John Doe", "J. Doe", A_CONTAINS_B], // first name to initial + ["John Doe", "J. doe", A_CONTAINS_B], + ["John Doe", "J. Doe", A_CONTAINS_B], // first name to initial without '.' + + ["John Middle Doe", "J. Middle Doe", A_CONTAINS_B], // first name to initial, middle name unchanged + ["John Middle Doe", "J. Doe", A_CONTAINS_B], // first name to initial, no middle name + + ["John Middle Doe", "John M. Doe", A_CONTAINS_B], // middle name to initial, first name unchanged + ["John Middle Doe", "J. M. Doe", A_CONTAINS_B], // first and middle name to initial + ["John Middle Doe", "J M Doe", A_CONTAINS_B], // first and middle name to initial without '.' + ["John Middle Doe", "John M. Doe", A_CONTAINS_B], // middle name with initial + + // Test Name Variants: multiple middle name + ["John Mary Jane Doe", "J. MARY JANE Doe", A_CONTAINS_B], // first to initial + ["John Mary Jane Doe", "john. M. J. doe", A_CONTAINS_B], // middle name to initial + ["John Mary Jane Doe", "J. M. J. Doe", A_CONTAINS_B], // first & middle name to initial + ["John Mary Jane Doe", "J. M. Doe", A_CONTAINS_B], // first & part of the middle name to initial + ["John Mary Jane Doe", "John M. Doe", A_CONTAINS_B], + ["John Mary Jane Doe", "J. Doe", A_CONTAINS_B], + + // Test Name Variants: merge initials + ["John Middle Doe", "JM Doe", A_CONTAINS_B], + ["John Mary Jane Doe", "JMJ. doe", A_CONTAINS_B], + + // Different: Don't consider the cases when family name is abbreviated + ["John Middle Doe", "JMD", DIFFERENT], + ["John Middle Doe", "John Middle D.", DIFFERENT], + ["John Middle Doe", "J. M. D.", DIFFERENT], +]; + +const TEST_FIELD_NAME = "Name"; + +add_setup(async () => {}); + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { name: value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { name: value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_organization.js b/browser/extensions/formautofill/test/unit/test_addressComponent_organization.js new file mode 100644 index 0000000000..6790b83599 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_organization.js @@ -0,0 +1,55 @@ +"use strict"; + +// prettier-ignore +const VALID_TESTS = [ + ["Mozilla", true], + ["mozilla", true], + ["@Mozilla", true], + [" ", true], // A string only contains whitespace is treated as empty, which is considered as valid + ["-!@#%&*_(){}[:;\"',.?]", false], // Not valid when the organization name only contains punctuations +]; + +const COMPARE_TESTS = [ + // Same + ["Mozilla", "Mozilla", SAME], // Exact the same + + // Similar + ["Mozilla", "mozilla", SIMILAR], // Ignore case + ["Casavant Frères", "Casavant Freres", SIMILAR], // asscent and base + ["Graphik Dimensions, Ltd.", "Graphik Dimensions Ltd", SIMILAR], // Punctuation is stripped and trim space in the end + ["T & T Supermarket", "T&T Supermarket", SIMILAR], // & is stripped and merged consecutive whitespace + ["Food & Pharmacy", "Pharmacy & Food", SIMILAR], // Same tokens, different order + ["Johnson & Johnson", "Johnson", SIMILAR], // Can always find the same token in the other + + // A Contains B + ["Mozilla Inc.", "Mozilla", A_CONTAINS_B], // Contain, the same prefix + ["The Walt Disney", "Walt Disney", A_CONTAINS_B], // Contain, the same suffix + ["Coca-Cola Company", "Coca Cola", A_CONTAINS_B], // Contain, strip punctuation + + // Different + ["Meta", "facebook", DIFFERENT], // Completely different + ["Metro Inc.", "CGI Inc.", DIFFERENT], // Different prefix + ["AT&T Corp.", "AT&T Inc.", DIFFERENT], // Different suffix + ["AT&T Corp.", "AT&T Corporation", DIFFERENT], // Different suffix + ["Ben & Jerry's", "Ben & Jerrys", DIFFERENT], // Different because Jerry's becomes ["Jerry", "s"] + ["Arc'teryx", "Arcteryx", DIFFERENT], // Different because Arc'teryx' becomes ["Arc", "teryx"] + ["BMW", "Bayerische Motoren Werke", DIFFERENT], + + ["Linens 'n Things", "Linens'n Things", SIMILAR], // Punctuation is replaced with whitespace, so both strings become "Linesns n Things" +]; + +const TEST_FIELD_NAME = "Organization"; + +add_setup(async () => {}); + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { organization: value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { organization: value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_postal_code.js b/browser/extensions/formautofill/test/unit/test_addressComponent_postal_code.js new file mode 100644 index 0000000000..a3150362d2 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_postal_code.js @@ -0,0 +1,57 @@ +"use strict"; + +const VALID_TESTS = [ + { region: "US" }, + ["1234", false], // too short + ["12345", true], + ["123456", false], // too long + ["1234A", false], // contain non-digit character + ["12345-123", false], + ["12345-1234", true], + ["12345-12345", false], + ["12345-1234A", false], + ["12345 1234", true], // Do we want to allow this? + ["12345_1234", false], // Do we want to allow this? + + { region: "CA" }, + ["M5T 1R5", true], + ["M5T1R5", true], // no space between the first and second parts is allowed + ["S4S 6X3", true], + ["M5T", false], // Only the first part + ["1R5", false], // Only the second part + ["D1B 1A1", false], // invalid first character, D + ["M5T 1R5A", false], // extra character at the end + ["M5T 1R5-", false], // extra character at the end + ["M5T-1R5", false], // hyphen in the wrong place + ["MT5 1R5", false], // missing letter in the first part + ["M5T 1R", false], // missing letter in the second part + ["M5T 1R55", false], // extra digit at the end + ["M5T 1R", false], // missing digit in the second part + ["M5T 1R5Q", false], // invalid second-to-last letter, Q +]; + +const COMPARE_TESTS = [ + { region: "US" }, + ["12345", "12345", SAME], + ["M5T 1R5", "m5t 1r5", SAME], + ["12345-1234", "12345 1234", SAME], + ["12345-1234", "12345", A_CONTAINS_B], + ["12345-1234", "12345#1234", SAME], // B is invalid + ["12345-1234", "1234", A_CONTAINS_B], // B is invalid +]; + +const TEST_FIELD_NAME = "PostalCode"; + +add_setup(async () => {}); + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { "postal-code": value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { "postal-code": value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_state.js b/browser/extensions/formautofill/test/unit/test_addressComponent_state.js new file mode 100644 index 0000000000..43e1de84dc --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_state.js @@ -0,0 +1,32 @@ +"use strict"; + +const VALID_TESTS = [ + ["California", true], + ["california", true], + ["Californib", false], + ["CA", true], + ["CA.", true], + ["CC", false], +]; + +const COMPARE_TESTS = [ + ["California", "california", SAME], // case insensitive + ["CA", "california", SAME], + ["CA", "ca", SAME], + ["California", "New Jersey", DIFFERENT], + ["New York", "New Jersey", DIFFERENT], +]; + +const TEST_FIELD_NAME = "State"; + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { "address-level1": value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { "address-level1": value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_street_address.js b/browser/extensions/formautofill/test/unit/test_addressComponent_street_address.js new file mode 100644 index 0000000000..83f1cd7ef3 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_street_address.js @@ -0,0 +1,56 @@ +"use strict"; + +const VALID_TESTS = [ + ["123 Main St. Apt 4, Floor 2", true], + ["This is a street", true], + ["A", true], + ["住址", true], + ["!#%&'*+", false], + ["1234", false], +]; + +const COMPARE_TESTS = [ + { region: "US" }, + ["123 Main St.", "123 Main St.", SAME], // Exactly the same with only street number and street name + ["123 Main St. Apt 4, Floor 2", "123 Main St. Apt 4, Floor 2", SAME], + ["123 Main St. Apt 4A, Floor 2", "123 main St. Apt 4a, Floor 2", SAME], + ["123 Main St. Apt 4, Floor 2", "123 Main St. Suite 4, 2nd fl", SAME], // Exactly the same after parsing + ["Main St.", "Main St.", SAME], // Exactly the same with only street name + ["Main St.", "main st.", SAME], // Exactly the same with only street name (case-insenstive) + + ["123 Main St.", "Main St.", A_CONTAINS_B], // Street number is mergeable + ["123 Main Lane St.", "123 Main St.", A_CONTAINS_B], // Street name is mergeable + ["123 Main St. Apt 4", "Main St.", A_CONTAINS_B], // Apartment number is mergeable + ["123 Main St. Apt 4, Floor 2", "123 Main St., Floor 2", A_CONTAINS_B], + ["123 Main St. Floor 2", "Main St.", A_CONTAINS_B], // Floor number is mergeable + ["123 Main St. Apt 4, Floor 2", "123 Main St. Apt 4", A_CONTAINS_B], + ["123 North-South Road", "123 North South Road", SIMILAR], // Street number is mergeable + + ["123 Main St. Apt 4, Floor 2", "1234 Main St. Apt 4, Floor 2", DIFFERENT], // Street number is different + ["123 Main St. Apt 4, Floor 2", "123 Mainn St. Apt 4, Floor 2", DIFFERENT], // Street name is different + [ + "123 Lane Main St. Apt 4, Floor 2", + "123 Main Lane St. Apt 4, Floor 2", + DIFFERENT, + ], // Street name is different (token not in order) + ["123 Main St. Apt 4, Floor 2", "123 Main St. Apt 41, Floor 2", DIFFERENT], // Apartment number is different + ["123 Main St. Apt 4, Floor 2", "123 Main St. Apt 4, Floor 22", DIFFERENT], // Floor number is different + + ["123 Main St. Apt 4, Floor 2", "123 Main St. Floor 2, Apt 4", DIFFERENT], // +]; + +const TEST_FIELD_NAME = "StreetAddress"; + +add_setup(async () => {}); + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { "street-address": value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { "street-address": value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_tel.js b/browser/extensions/formautofill/test/unit/test_addressComponent_tel.js new file mode 100644 index 0000000000..584ab9f8f3 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_tel.js @@ -0,0 +1,76 @@ +/* import-globals-from head_addressComponent.js */ + +"use strict"; + +// prettier-ignore +const VALID_TESTS = [ + // US Valid format (XXX-XXX-XXXX) and first digit is between 2-9 + ["200-234-5678", true], // First digit should between 2-9 + ["100-234-5678", true], // First digit is not between 2-9, but currently not being checked + // when no country code is specified + ["555-abc-1234", true], // Non-digit characters are normalized according to ITU E.161 standard + ["55-555-5555", false], // The national number is too short (9 digits) + + ["2-800-555-1234", false], // "2" is not US country code so we treat + // 2-800-555-1234 as the national number, which is too long (11 digits) + + // Phone numbers with country code + ["1-800-555-1234", true], // Country code without plus sign + ["+1 200-234-5678", true], // Country code with plus sign and with a valid national number + ["+1 100-234-5678", false], // National number should be between 2-9 + ["+1 55-555-5555", false], // National number is too short (9 digits) + ["+1 1-800-555-1234", true], // "+1" and "1" are both treated as coutnry code so national number + // is a valid number (800-555-1234) + ["+1 2-800-555-1234", false], // The national number is too long (11 digits) + ["+1 555-abc-1234", true], // Non-digit characters are normalized according to ITU E.161 standard +]; + +const COMPARE_TESTS = [ + ["+1 520-248-6621", "+15202486621", SAME], + ["+1 520-248-6621", "1-520-248-6621", SAME], + ["+1 520-248-6621", "1(520)248-6621", SAME], + ["520-248-6621", "520-248-6621", SAME], // Both phone numbers don't have coutry code + ["520-248-6621", "+1 520-248-6621", SAME], // Compare phone number with and without country code + + ["+1 520-248-6621", "248-6621", A_CONTAINS_B], + ["520-248-6621", "248-6621", A_CONTAINS_B], + ["0520-248-6621", "520-248-6621", A_CONTAINS_B], + ["48-6621", "6621", A_CONTAINS_B], // Both phone number are invalid + + ["+1 520-248-6621", "+91 520-248-6622", DIFFERENT], // different national prefix and number + ["+1 520-248-6621", "+91 520-248-6621", DIFFERENT], // Same number, different national prefix + ["+1 520-248-6621", "+1 520-248-6622", DIFFERENT], // Same national prefix, different number + ["520-248-6621", "+91 520-248-6622", DIFFERENT], // Same test as above but with default region + ["520-248-6621", "+91 520-248-6621", DIFFERENT], // Same test as above but with default region + ["520-248-6621", "+1 520-248-6622", DIFFERENT], // Same test as above but with default region + ["520-248-6621", "520-248-6622", DIFFERENT], + + // Normalize + ["+1 520-248-6621", "+1 ja0-bgt-mnc1", SAME], + ["+1 1-800-555-1234", "+1 800-555-1234", SAME], + + // TODO: Support extension + //["+64 3 331-6005", "3 331 6005#1234", A_CONTAINS_B], +]; + +const TEST_FIELD_NAME = "Tel"; + +add_setup(async () => { + Services.prefs.setBoolPref("browser.search.region", "US"); + + registerCleanupFunction(function head_cleanup() { + Services.prefs.clearUserPref("browser.search.region"); + }); +}); + +add_task(async function test_isValid() { + runIsValidTest(VALID_TESTS, TEST_FIELD_NAME, value => { + return { tel: value }; + }); +}); + +add_task(async function test_compare() { + runCompareTest(COMPARE_TESTS, TEST_FIELD_NAME, value => { + return { tel: value }; + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressDataLoader.js b/browser/extensions/formautofill/test/unit/test_addressDataLoader.js new file mode 100644 index 0000000000..6b9cfb102c --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressDataLoader.js @@ -0,0 +1,102 @@ +"use strict"; + +const SUPPORT_COUNTRIES_TESTCASES = [ + { + country: "US", + properties: ["languages", "alternative_names", "sub_keys", "sub_names"], + }, + { + country: "CA", + properties: ["languages", "name", "sub_keys", "sub_names"], + }, + { + country: "DE", + properties: ["name"], + }, +]; + +var AddressDataLoader, FormAutofillUtils; +add_setup(async () => { + ({ AddressDataLoader, FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" + )); +}); + +add_task(async function test_initalState() { + // addressData should not exist + Assert.equal(AddressDataLoader._addressData, undefined); + // Verify _dataLoaded state + Assert.equal(AddressDataLoader._dataLoaded.country, false); + Assert.equal(AddressDataLoader._dataLoaded.level1.size, 0); +}); + +add_task(async function test_loadDataState() { + sinon.spy(AddressDataLoader, "_loadScripts"); + let metadata = FormAutofillUtils.getCountryAddressData("US"); + Assert.ok(AddressDataLoader._addressData, "addressData exists"); + // Verify _dataLoaded state + Assert.equal(AddressDataLoader._dataLoaded.country, true); + Assert.equal(AddressDataLoader._dataLoaded.level1.size, 0); + // _loadScripts should be called + sinon.assert.called(AddressDataLoader._loadScripts); + // Verify metadata + Assert.equal(metadata.id, "data/US"); + Assert.ok( + metadata.alternative_names, + "US alternative names should be loaded from extension" + ); + AddressDataLoader._loadScripts.resetHistory(); + + // Load data without country + let newMetadata = FormAutofillUtils.getCountryAddressData(); + // _loadScripts should not be called + sinon.assert.notCalled(AddressDataLoader._loadScripts); + Assert.deepEqual( + metadata, + newMetadata, + "metadata should be US if country is not specified" + ); + AddressDataLoader._loadScripts.resetHistory(); + + // Load level 1 data that does not exist + let undefinedMetadata = FormAutofillUtils.getCountryAddressData("US", "CA"); + // _loadScripts should be called + sinon.assert.called(AddressDataLoader._loadScripts); + Assert.equal(undefinedMetadata, undefined, "metadata should be undefined"); + Assert.ok( + AddressDataLoader._dataLoaded.level1.has("US"), + "level 1 state array should be set even there's no valid metadata" + ); + AddressDataLoader._loadScripts.resetHistory(); + + // Load level 1 data again + undefinedMetadata = FormAutofillUtils.getCountryAddressData("US", "AS"); + Assert.equal(undefinedMetadata, undefined, "metadata should be undefined"); + // _loadScripts should not be called + sinon.assert.notCalled(AddressDataLoader._loadScripts); +}); + +SUPPORT_COUNTRIES_TESTCASES.forEach(testcase => { + add_task(async function test_support_country() { + info("Starting testcase: Check " + testcase.country + " metadata"); + let metadata = FormAutofillUtils.getCountryAddressData(testcase.country); + Assert.ok( + testcase.properties.every(key => metadata[key]), + "These properties should exist: " + testcase.properties + ); + // Verify the multi-locale country + if (metadata.languages && metadata.languages.length > 1) { + let locales = FormAutofillUtils.getCountryAddressDataWithLocales( + testcase.country + ); + Assert.equal( + metadata.languages.length, + locales.length, + "Total supported locales should be matched" + ); + metadata.languages.forEach((lang, index) => { + Assert.equal(lang, locales[index].lang, `Should support ${lang}`); + }); + } + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_addressRecords.js b/browser/extensions/formautofill/test/unit/test_addressRecords.js new file mode 100644 index 0000000000..53db04ee38 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressRecords.js @@ -0,0 +1,858 @@ +/** + * Tests FormAutofillStorage object with addresses records. + */ + +"use strict"; + +const TEST_STORE_FILE_NAME = "test-profile.json"; +const COLLECTION_NAME = "addresses"; + +const TEST_ADDRESS_1 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+16172535702", + email: "timbl@w3.org", + "unknown-1": "an unknown field from another client", +}; + +const TEST_ADDRESS_2 = { + "street-address": "Some Address", + country: "US", +}; + +const TEST_ADDRESS_3 = { + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "Other Address", + "postal-code": "12345", +}; + +const TEST_ADDRESS_4 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", +}; + +const TEST_ADDRESS_WITH_EMPTY_FIELD = { + name: "Tim Berners", + "street-address": "", +}; + +const TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD = { + name: "", + "address-line1": "", + "address-line2": "", + "address-line3": "", + "country-name": "", + "tel-country-code": "", + "tel-national": "", + "tel-area-code": "", + "tel-local": "", + "tel-local-prefix": "", + "tel-local-suffix": "", + email: "timbl@w3.org", +}; + +const TEST_ADDRESS_WITH_INVALID_FIELD = { + "street-address": "Another Address", + email: { email: "invalidemail" }, +}; + +const TEST_ADDRESS_EMPTY_AFTER_NORMALIZE = { + country: "XXXXXX", +}; + +const TEST_ADDRESS_EMPTY_AFTER_UPDATE_ADDRESS_2 = { + "street-address": "", + country: "XXXXXX", +}; + +const MERGE_TESTCASES = [ + { + description: "Merge a superset", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + "unknown-1": "an unknown field from another client", + }, + addressToMerge: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + "unknown-1": "an unknown field from another client", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + "unknown-1": "an unknown field from another client", + }, + }, + { + description: "Loose merge a subset", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + addressToMerge: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + noNeedToUpdate: true, + }, + { + description: "Strict merge a subset without empty string", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + addressToMerge: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + strict: true, + noNeedToUpdate: true, + }, + { + description: "Merge an address with partial overlaps", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + }, + { + description: + "Merge an address with multi-line street-address in storage and single-line incoming one", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E. Evelyn Avenue Line2", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2", + tel: "+16509030800", + country: "US", + }, + }, + { + description: + "Merge an address with 3-line street-address in storage and 2-line incoming one", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E. Evelyn Avenue\nLine2 Line3", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + country: "US", + }, + }, + { + description: + "Merge an address with single-line street-address in storage and multi-line incoming one", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue Line2", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E. Evelyn Avenue\nLine2", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2", + tel: "+16509030800", + country: "US", + }, + }, + { + description: + "Merge an address with 2-line street-address in storage and 3-line incoming one", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2 Line3", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + country: "US", + }, + }, + { + description: "Merge an address with the same amount of lines", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E. Evelyn\nAvenue Line2\nLine3", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + country: "US", + }, + }, + { + description: + "Merge an address with superfluous external and internal whitespace in the street-address", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": " 331 E. Evelyn\n Avenue Line2\n Line3 ", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", + tel: "+16509030800", + country: "US", + }, + }, + { + description: "Merge an address with collapsed whitespace", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331 E.Evelyn Avenue", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + }, + { + description: "Merge an address with punctuation and mIxEd-cAsE", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331.e.EVELYN AVENUE", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Avenue", + tel: "+16509030800", + country: "US", + }, + }, + { + description: "Merge an address with accent characters", + addressInStorage: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Straße", + tel: "+16509030800", + }, + addressToMerge: { + "street-address": "331.e.EVELYN Strasse", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + "street-address": "331 E. Evelyn Straße", + tel: "+16509030800", + country: "US", + }, + }, + { + description: "Merge an address with a mIxEd-cAsE name", + addressInStorage: { + "given-name": "Timothy", + tel: "+16509030800", + }, + addressToMerge: { + "given-name": "TIMOTHY", + tel: "+16509030800", + country: "US", + }, + expectedAddress: { + "given-name": "Timothy", + tel: "+16509030800", + country: "US", + }, + }, +]; + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +let do_check_record_matches = (recordWithMeta, record) => { + for (let key in record) { + Assert.equal(recordWithMeta[key], record[key]); + } +}; + +add_task(async function test_initialize() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + + Assert.equal(profileStorage._store.data.version, 1); + Assert.equal(profileStorage._store.data.addresses.length, 0); + + let data = profileStorage._store.data; + Assert.deepEqual(data.addresses, []); + + await profileStorage._saveImmediately(); + + profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + + Assert.deepEqual(profileStorage._store.data, data); + for (let { _sync } of profileStorage._store.data.addresses) { + Assert.ok(_sync); + Assert.equal(_sync.changeCounter, 1); + } +}); + +add_task(async function test_getAll() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + + Assert.equal(addresses.length, 2); + do_check_record_matches(addresses[0], TEST_ADDRESS_1); + do_check_record_matches(addresses[1], TEST_ADDRESS_2); + + // Check computed fields. + Assert.equal(addresses[0].name, "Timothy John Berners-Lee"); + Assert.equal(addresses[0]["address-line1"], "32 Vassar Street"); + Assert.equal(addresses[0]["address-line2"], "MIT Room 32-G524"); + + // Test with rawData set. + addresses = await profileStorage.addresses.getAll({ rawData: true }); + Assert.equal(addresses[0].name, undefined); + Assert.equal(addresses[0]["address-line1"], undefined); + Assert.equal(addresses[0]["address-line2"], undefined); + + // Modifying output shouldn't affect the storage. + addresses[0].organization = "test"; + do_check_record_matches( + (await profileStorage.addresses.getAll())[0], + TEST_ADDRESS_1 + ); +}); + +add_task(async function test_get() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[0].guid; + + let address = await profileStorage.addresses.get(guid); + do_check_record_matches(address, TEST_ADDRESS_1); + + // Test with rawData set. + address = await profileStorage.addresses.get(guid, { rawData: true }); + Assert.equal(address.name, undefined); + Assert.equal(address["address-line1"], undefined); + Assert.equal(address["address-line2"], undefined); + + // Modifying output shouldn't affect the storage. + address.organization = "test"; + do_check_record_matches( + await profileStorage.addresses.get(guid), + TEST_ADDRESS_1 + ); + + Assert.equal(await profileStorage.addresses.get("INVALID_GUID"), null); +}); + +add_task(async function test_add() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + + Assert.equal(addresses.length, 2); + + do_check_record_matches(addresses[0], TEST_ADDRESS_1); + do_check_record_matches(addresses[1], TEST_ADDRESS_2); + + Assert.notEqual(addresses[0].guid, undefined); + Assert.equal(addresses[0].version, 1); + Assert.notEqual(addresses[0].timeCreated, undefined); + Assert.equal(addresses[0].timeLastModified, addresses[0].timeCreated); + Assert.equal(addresses[0].timeLastUsed, 0); + Assert.equal(addresses[0].timesUsed, 0); + + // Empty string should be deleted before saving. + await profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_FIELD); + let address = profileStorage.addresses._data[2]; + Assert.equal(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name); + Assert.equal(address["street-address"], undefined); + + // Empty computed fields shouldn't cause any problem. + await profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD); + address = profileStorage.addresses._data[3]; + Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email); + + await Assert.rejects( + profileStorage.addresses.add(TEST_ADDRESS_WITH_INVALID_FIELD), + /"email" contains invalid data type: object/ + ); + + await Assert.rejects( + profileStorage.addresses.add({}), + /Record contains no valid field\./ + ); + + await Assert.rejects( + profileStorage.addresses.add(TEST_ADDRESS_EMPTY_AFTER_NORMALIZE), + /Record contains no valid field\./ + ); +}); + +add_task(async function test_update() { + // Test assumes that when an entry is saved a second time, it's last modified date will + // be different from the first. With high values of precision reduction, we execute too + // fast for that to be true. + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function () { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[1].guid; + // We need to cheat a little due to race conditions of Date.now() when + // we're running these tests, so we subtract one and test accordingly + // in the times Date.now() returns the same timestamp + let timeLastModified = addresses[1].timeLastModified - 1; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "update" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + + Assert.notEqual(addresses[1].country, undefined); + + await profileStorage.addresses.update(guid, TEST_ADDRESS_3); + await onChanged; + await profileStorage._saveImmediately(); + + profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below. + + let address = await profileStorage.addresses.get(guid, { rawData: true }); + + Assert.equal(address.country, undefined); + Assert.ok(address.timeLastModified > timeLastModified); + do_check_record_matches(address, TEST_ADDRESS_3); + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + + // Test preserveOldProperties parameter and field with empty string. + await profileStorage.addresses.update( + guid, + TEST_ADDRESS_WITH_EMPTY_FIELD, + true + ); + await onChanged; + await profileStorage._saveImmediately(); + + profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below. + + address = await profileStorage.addresses.get(guid, { rawData: true }); + + Assert.equal(address["given-name"], "Tim"); + Assert.equal(address["family-name"], "Berners"); + Assert.equal(address["street-address"], undefined); + Assert.equal(address["postal-code"], "12345"); + Assert.notEqual(address.timeLastModified, timeLastModified); + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 2); + + // Empty string should be deleted while updating. + await profileStorage.addresses.update( + profileStorage.addresses._data[0].guid, + TEST_ADDRESS_WITH_EMPTY_FIELD + ); + address = profileStorage.addresses._data[0]; + Assert.equal(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name); + Assert.equal(address["street-address"], undefined); + Assert.equal(address[("unknown-1", "an unknown field from another client")]); + + // Empty computed fields shouldn't cause any problem. + await profileStorage.addresses.update( + profileStorage.addresses._data[0].guid, + TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD, + false + ); + address = profileStorage.addresses._data[0]; + Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email); + await profileStorage.addresses.update( + profileStorage.addresses._data[1].guid, + TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD, + true + ); + address = profileStorage.addresses._data[1]; + Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email); + + await Assert.rejects( + profileStorage.addresses.update("INVALID_GUID", TEST_ADDRESS_3), + /No matching record\./ + ); + + await Assert.rejects( + profileStorage.addresses.update(guid, TEST_ADDRESS_WITH_INVALID_FIELD), + /"email" contains invalid data type: object/ + ); + + await Assert.rejects( + profileStorage.addresses.update(guid, {}), + /Record contains no valid field\./ + ); + + await Assert.rejects( + profileStorage.addresses.update(guid, TEST_ADDRESS_EMPTY_AFTER_NORMALIZE), + /Record contains no valid field\./ + ); + + profileStorage.addresses.update(guid, TEST_ADDRESS_2); + await Assert.rejects( + profileStorage.addresses.update( + guid, + TEST_ADDRESS_EMPTY_AFTER_UPDATE_ADDRESS_2 + ), + /Record contains no valid field\./ + ); +}); + +add_task(async function test_notifyUsed() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[1].guid; + let timeLastUsed = addresses[1].timeLastUsed; + let timesUsed = addresses[1].timesUsed; + + profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below. + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "notifyUsed" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + + profileStorage.addresses.notifyUsed(guid); + await onChanged; + + let address = await profileStorage.addresses.get(guid); + + Assert.equal(address.timesUsed, timesUsed + 1); + Assert.notEqual(address.timeLastUsed, timeLastUsed); + + // Using a record should not bump its change counter. + Assert.equal( + getSyncChangeCounter(profileStorage.addresses, guid), + changeCounter + ); + + Assert.throws( + () => profileStorage.addresses.notifyUsed("INVALID_GUID"), + /No matching record\./ + ); +}); + +add_task(async function test_remove() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[1].guid; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "remove" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + + Assert.equal(addresses.length, 2); + + profileStorage.addresses.remove(guid); + await onChanged; + + addresses = await profileStorage.addresses.getAll(); + + Assert.equal(addresses.length, 1); + + Assert.equal(await profileStorage.addresses.get(guid), null); +}); + +MERGE_TESTCASES.forEach(testcase => { + add_task(async function test_merge() { + info("Starting testcase: " + testcase.description); + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + testcase.addressInStorage, + ]); + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[0].guid; + // We need to cheat a little due to race conditions of Date.now() when + // we're running these tests, so we subtract one and test accordingly + // in the times Date.now() returns the same timestamp + let timeLastModified = addresses[0].timeLastModified - 1; + + // Merge address and verify the guid in notifyObservers subject + let onMerged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "update" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + + // Force to create sync metadata. + profileStorage.addresses.pullSyncChanges(); + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + + Assert.ok( + profileStorage.addresses.mergeIfPossible( + guid, + testcase.addressToMerge, + testcase.strict + ) + ); + if (!testcase.noNeedToUpdate) { + await onMerged; + } + + addresses = await profileStorage.addresses.getAll(); + Assert.equal(addresses.length, 1); + do_check_record_matches(addresses[0], testcase.expectedAddress); + if (testcase.noNeedToUpdate) { + // see timeLastModified for why we check -1 + Assert.equal(addresses[0].timeLastModified - 1, timeLastModified); + + // No need to bump the change counter if the data is unchanged. + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + } else { + Assert.ok(addresses[0].timeLastModified > timeLastModified); + + // Record merging should bump the change counter. + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 2); + } + }); +}); + +add_task(async function test_merge_same_address() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[0].guid; + let timeLastModified = addresses[0].timeLastModified; + + // Force to create sync metadata. + profileStorage.addresses.pullSyncChanges(); + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + + // Merge same address will still return true but it won't update timeLastModified. + Assert.ok(profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_1)); + Assert.equal(addresses[0].timeLastModified, timeLastModified); + + // ... and won't bump the change counter, either. + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); +}); + +add_task(async function test_merge_unable_merge() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + let guid = addresses[1].guid; + + // Force to create sync metadata. + profileStorage.addresses.pullSyncChanges(); + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + + // Unable to merge because of conflict + Assert.equal( + await profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_3), + false + ); + + // Unable to merge because no overlap + Assert.equal( + await profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_4), + false + ); + + // Unable to strict merge because subset with empty string + let subset = Object.assign({}, TEST_ADDRESS_1); + subset.organization = ""; + Assert.equal( + await profileStorage.addresses.mergeIfPossible(guid, subset, true), + false + ); + + // Shouldn't bump the change counter + Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); +}); + +add_task(async function test_mergeToStorage() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + // Merge an address to storage + let anotherAddress = profileStorage.addresses._clone(TEST_ADDRESS_2); + await profileStorage.addresses.add(anotherAddress); + anotherAddress.email = "timbl@w3.org"; + Assert.equal( + (await profileStorage.addresses.mergeToStorage(anotherAddress)).length, + 2 + ); + + Assert.equal( + (await profileStorage.addresses.getAll())[1].email, + anotherAddress.email + ); + Assert.equal( + (await profileStorage.addresses.getAll())[2].email, + anotherAddress.email + ); + + // Empty computed fields shouldn't cause any problem. + Assert.equal( + ( + await profileStorage.addresses.mergeToStorage( + TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD + ) + ).length, + 3 + ); +}); + +add_task(async function test_mergeToStorage_strict() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + // Try to merge a subset with empty string + let anotherAddress = profileStorage.addresses._clone(TEST_ADDRESS_1); + anotherAddress.email = ""; + Assert.equal( + (await profileStorage.addresses.mergeToStorage(anotherAddress, true)) + .length, + 0 + ); + Assert.equal( + (await profileStorage.addresses.getAll())[0].email, + TEST_ADDRESS_1.email + ); + + // Empty computed fields shouldn't cause any problem. + Assert.equal( + ( + await profileStorage.addresses.mergeToStorage( + TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD, + true + ) + ).length, + 1 + ); +}); diff --git a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js new file mode 100644 index 0000000000..70de21cfe5 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js @@ -0,0 +1,1078 @@ +/* + * Test for form auto fill content helper fill all inputs function. + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +const { setTimeout, clearTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" +); + +const TESTCASES = [ + { + description: "Form without autocomplete property", + document: `
    + +
    `, + focusedInputId: "given-name", + profileData: {}, + expectedResult: { + "street-addr": "", + city: "", + country: "", + email: "", + tel: "", + }, + }, + { + description: "Form with autocomplete properties and 1 token", + document: `
    + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "street-address": "2 Harrison St line2", + "-moz-street-address-one-line": "2 Harrison St line2", + "address-level2": "San Francisco", + country: "US", + email: "foo@mozilla.com", + tel: "1234567", + }, + expectedResult: { + "street-addr": "2 Harrison St line2", + city: "San Francisco", + country: "US", + email: "foo@mozilla.com", + tel: "1234567", + }, + }, + { + description: "Form with autocomplete properties and 2 tokens", + document: `
    + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "street-address": "2 Harrison St", + "address-level2": "San Francisco", + country: "US", + email: "foo@mozilla.com", + tel: "1234567", + }, + expectedResult: { + "street-addr": "2 Harrison St", + city: "San Francisco", + country: "US", + email: "foo@mozilla.com", + tel: "1234567", + }, + }, + { + description: + "Form with autocomplete properties and profile is partly matched", + document: `
    + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "street-address": "2 Harrison St", + "address-level2": "San Francisco", + country: "US", + email: "", + tel: "", + }, + expectedResult: { + "street-addr": "2 Harrison St", + city: "San Francisco", + country: "US", + email: "", + tel: "", + }, + }, + { + description: "Form with autocomplete properties but mismatched", + document: `
    + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "street-address": "", + "address-level2": "", + country: "", + email: "foo@mozilla.com", + tel: "1234567", + }, + expectedResult: { + "street-addr": "", + city: "", + country: "", + email: "foo@mozilla.com", + tel: "1234567", + }, + }, + { + description: `Form with elements that have autocomplete set to "off"`, + document: `
    + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "given-name": "John", + "family-name": "Doe", + "street-address": "2 Harrison St", + country: "US", + organization: "Test organization", + }, + expectedResult: { + "given-name": "John", + "family-name": "Doe", + "street-address": "2 Harrison St", + organization: "Test organization", + country: "US", + }, + }, + { + description: `Form with autocomplete set to "off" and no autocomplete attribute on the form's elements`, + document: `
    + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "given-name": "John", + "family-name": "Doe", + "street-address": "2 Harrison St", + country: "US", + "address-level2": "Somewhere", + }, + expectedResult: { + "given-name": "John", + "family-name": "Doe", + "street-address": "2 Harrison St", + city: "Somewhere", + country: "US", + }, + }, + { + description: + "Form with autocomplete select elements and matching option values", + document: `
    + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + "address-level1": "CA", + }, + expectedResult: { + country: "US", + state: "CA", + }, + }, + { + description: + "Form with autocomplete select elements and matching option texts", + document: `
    + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "United States", + "address-level1": "California", + }, + expectedResult: { + country: "US", + state: "CA", + }, + }, + { + description: "Form with a readonly input and non-readonly inputs", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "given-name": "John", + "family-name": "Doe", + "street-address": "100 Main Street", + city: "Hamilton", + }, + expectedResult: { + "given-name": "John", + "family-name": "Doe", + "street-addr": "100 Main Street", + city: "TEST CITY", + }, + }, + { + description: "Fill address fields in a form with addr and CC fields.", + document: `
    + + + + + + + + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "street-address": "2 Harrison St line2", + "-moz-street-address-one-line": "2 Harrison St line2", + "address-level2": "San Francisco", + country: "US", + email: "foo@mozilla.com", + tel: "1234567", + }, + expectedResult: { + "street-addr": "2 Harrison St line2", + city: "San Francisco", + country: "US", + email: "foo@mozilla.com", + tel: "1234567", + "cc-number": "", + "cc-name": "", + "cc-exp-month": "", + "cc-exp-year": "", + }, + }, + { + description: + "Fill credit card fields in a form with address and CC fields.", + document: `
    + + + + + + + + + + + +
    `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + "street-addr": "", + city: "", + country: "", + email: "", + tel: "", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": "06", + "cc-exp-year": "25", + }, + }, + { + description: + "Fill credit card fields in a form with a placeholder on expiration month input field", + document: `
    + + + + +
    + `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": "06", + "cc-exp-year": "25", + }, + }, + { + description: + "Fill credit card fields in a form without a placeholder on expiration month and expiration year input fields", + document: `
    + + + + +
    + `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": "06", + "cc-exp-year": "25", + }, + }, + { + description: + "Fill credit card fields in a form with a placeholder on expiration year input field", + document: `
    + + + + +
    + `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 2025, + }, + expectedResult: { + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": "06", + "cc-exp-year": "25", + }, + }, + { + description: + "Form with hidden input and visible input that share the same autocomplete attribute", + document: `
    + + + + + + + + +
    `, + focusedInputId: "visible-cc", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + guid: "123", + "visible-cc": "4111111111111111", + "visible-name": "test name", + "cc-exp-month": "06", + "cc-exp-year": "25", + "hidden-cc": undefined, + "hidden-cc-2": undefined, + "hidden-name": undefined, + "hidden-name-2": undefined, + }, + }, + { + description: + "Fill credit card fields in a form where the value property is being used as a placeholder for cardholder name", + document: `
    + + + + +
    `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": "06", + "cc-exp-year": "25", + }, + }, + { + description: + "Fill credit card number fields in a form with multiple cc-number inputs", + document: `
    + + + + + + +
    `, + focusedInputId: "cc-number1", + profileData: { + guid: "123", + "cc-number": "371449635398431", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + guid: "123", + "cc-number1": "3714", + "cc-number2": "4963", + "cc-number3": "5398", + "cc-number4": "431", + "cc-exp-month": "06", + "cc-exp-year": "25", + }, + }, + { + description: + "Fill credit card number fields in a form with multiple valid credit card sections", + document: `
    + + + + + + + + + + + + + + + + + +
    + `, + focusedInputId: "cc-number1", + profileData: { + guid: "123", + "cc-type": "mastercard", + "cc-number": "371449635398431", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + guid: "123", + "cc-type1": "mastercard", + "cc-number1": "3714", + "cc-number2": "4963", + "cc-number3": "5398", + "cc-number4": "431", + "cc-exp-month1": "06", + "cc-exp-year1": "25", + "cc-type2": "", + "cc-number-5": "", + "cc-number-6": "", + "cc-number-7": "", + "cc-number-8": "", + "cc-exp-month2": "", + "cc-exp-year2": "", + }, + }, + { + description: + "Fill credit card fields in a form with placeholders on month and year and these inputs are type=tel", + document: `
    + + + + +
    + `, + focusedInputId: "cardHolder", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 2025, + }, + expectedResult: { + cardHolder: "test name", + cardNumber: "4111111111111111", + month: "06", + year: "25", + }, + }, +]; + +const TESTCASES_INPUT_UNCHANGED = [ + { + description: + "Form with autocomplete select elements; with default and no matching options", + document: `
    + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + "address-level1": "unknown state", + }, + expectedResult: { + country: "US", + state: "", + }, + }, +]; + +const TESTCASES_FILL_SELECT = [ + // US States + { + description: "Form with US states select elements", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + "address-level1": "CA", + }, + expectedResult: { + state: "CA", + }, + }, + { + description: + "Form with US states select elements; with lower case state key", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + "address-level1": "CA", + }, + expectedResult: { + state: "ca", + }, + }, + { + description: + "Form with US states select elements; with state name and extra spaces", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + "address-level1": " California ", + }, + expectedResult: { + state: "CA", + }, + }, + { + description: + "Form with US states select elements; with partial state key match", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + "address-level1": "WA", + }, + expectedResult: { + state: "US-WA", + }, + }, + + // Country + { + description: "Form with country select elements", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + }, + expectedResult: { + country: "US", + }, + }, + { + description: "Form with country select elements; with lower case key", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + }, + expectedResult: { + country: "us", + }, + }, + { + description: "Form with country select elements; with alternative name 1", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + }, + expectedResult: { + country: "XX", + }, + }, + { + description: "Form with country select elements; with alternative name 2", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + }, + expectedResult: { + country: "XX", + }, + }, + { + description: + "Form with country select elements; with partial matching value", + document: `
    + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + country: "US", + }, + expectedResult: { + country: "XX", + }, + }, + { + description: + "Fill credit card expiration month field in a form with select field", + document: `
    + + +
    `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResult: { + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": "6", + "cc-exp-year": "2025", + }, + }, + { + description: + "Fill credit card information correctly when one of the card type options is 'American Express'", + document: `
    + + + + + +
    `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "378282246310005", + "cc-type": "amex", + "cc-name": "test name", + "cc-exp-month": 8, + "cc-exp-year": 26, + }, + expectedResult: { + guid: "123", + "cc-number": "378282246310005", + "cc-type": "AX", + "cc-name": "test name", + "cc-exp-month": 8, + "cc-exp-year": 26, + }, + }, +]; + +const TESTCASES_BOTH_CHANGED_AND_UNCHANGED = [ + { + description: + "Form with a disabled input and non-disabled inputs. The 'country' field should not change", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + guid: "123", + "given-name": "John", + "family-name": "Doe", + "street-address": "100 Main Street", + country: "CA", + }, + expectedResult: { + "given-name": "John", + "family-name": "Doe", + "street-addr": "100 Main Street", + country: "DE", + }, + }, +]; + +function do_test(testcases, testFn) { + for (let tc of testcases) { + (function () { + let testcase = tc; + add_task(async function () { + info("Starting testcase: " + testcase.description); + let ccNumber = testcase.profileData["cc-number"]; + if (ccNumber) { + testcase.profileData["cc-number-encrypted"] = + await OSKeyStore.encrypt(ccNumber); + delete testcase.profileData["cc-number"]; + } + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + let form = doc.querySelector("form"); + let formLike = FormLikeFactory.createFromForm(form); + let handler = new FormAutofillHandler(formLike); + let promises = []; + // Replace the internal decrypt method with OSKeyStore API, + // but don't pass the reauth parameter to avoid triggering + // reauth login dialog in these tests. + let decryptHelper = async (cipherText, reauth) => { + return OSKeyStore.decrypt(cipherText, false); + }; + handler.collectFormFields(); + + let focusedInput = doc.getElementById(testcase.focusedInputId); + try { + handler.focusedInput = focusedInput; + } catch (e) { + if (e.message.includes("WeakMap key must be an object")) { + throw new Error( + `Couldn't find the focusedInputId in the current form! Make sure focusedInputId exists in your test form! testcase description:${testcase.description}` + ); + } else { + throw e; + } + } + + for (let section of handler.sections) { + section._decrypt = decryptHelper; + } + + handler.activeSection.fieldDetails.forEach(field => { + let element = field.elementWeakRef.get(); + if (!testcase.profileData[field.fieldName]) { + // Avoid waiting for `change` event of a input with a blank value to + // be filled. + return; + } + promises.push(...testFn(testcase, element)); + }); + + let [adaptedProfile] = handler.activeSection.getAdaptedProfiles([ + testcase.profileData, + ]); + await handler.autofillFormFields(adaptedProfile, focusedInput); + Assert.equal( + handler.activeSection.filledRecordGUID, + testcase.profileData.guid, + "Check if filledRecordGUID is set correctly" + ); + await Promise.all(promises); + }); + })(); + } +} + +do_test(TESTCASES, (testcase, element) => { + let id = element.id; + return [ + new Promise(resolve => { + element.addEventListener( + "input", + () => { + Assert.ok(true, "Checking " + id + " field fires input event"); + resolve(); + }, + { once: true } + ); + }), + new Promise(resolve => { + element.addEventListener( + "change", + () => { + Assert.ok(true, "Checking " + id + " field fires change event"); + Assert.equal( + element.value, + testcase.expectedResult[id], + "Check the " + id + " field was filled with correct data" + ); + resolve(); + }, + { once: true } + ); + }), + ]; +}); + +do_test(TESTCASES_INPUT_UNCHANGED, (testcase, element) => { + return [ + new Promise((resolve, reject) => { + // Make sure no change or input event is fired when no change occurs. + let cleaner; + let timer = setTimeout(() => { + let id = element.id; + element.removeEventListener("change", cleaner); + element.removeEventListener("input", cleaner); + Assert.equal( + element.value, + testcase.expectedResult[id], + "Check no value is changed on the " + id + " field" + ); + resolve(); + }, 1000); + cleaner = event => { + clearTimeout(timer); + reject(`${event.type} event should not fire`); + }; + element.addEventListener("change", cleaner); + element.addEventListener("input", cleaner); + }), + ]; +}); + +do_test(TESTCASES_FILL_SELECT, (testcase, element) => { + let id = element.id; + return [ + new Promise(resolve => { + element.addEventListener( + "input", + () => { + Assert.equal( + element.value, + testcase.expectedResult[id], + "Check the " + id + " field was filled with correct data" + ); + resolve(); + }, + { once: true } + ); + }), + ]; +}); + +do_test(TESTCASES_BOTH_CHANGED_AND_UNCHANGED, (testcase, element) => { + // Ensure readonly and disabled inputs are not autofilled + if (element.readOnly || element.disabled) { + return [ + new Promise((resolve, reject) => { + // Make sure no change or input event is fired when no change occurs. + let cleaner; + let timer = setTimeout(() => { + let id = element.id; + element.removeEventListener("change", cleaner); + element.removeEventListener("input", cleaner); + Assert.equal( + element.value, + testcase.expectedResult[id], + "Check no value is changed on the " + id + " field" + ); + resolve(); + }, 1000); + cleaner = event => { + clearTimeout(timer); + reject(`${event.type} event should not fire`); + }; + element.addEventListener("change", cleaner); + element.addEventListener("input", cleaner); + }), + ]; + } + let id = element.id; + // Ensure that non-disabled and non-readonly fields are filled correctly + return [ + new Promise(resolve => { + element.addEventListener( + "input", + () => { + Assert.ok(true, "Checking " + id + " field fires input event"); + resolve(); + }, + { once: true } + ); + }), + new Promise(resolve => { + element.addEventListener( + "change", + () => { + Assert.ok(true, "Checking " + id + " field fires change event"); + Assert.equal( + element.value, + testcase.expectedResult[id], + "Check the " + id + " field was filled with correct data" + ); + resolve(); + }, + { once: true } + ); + }), + ]; +}); diff --git a/browser/extensions/formautofill/test/unit/test_clearPopulatedForm.js b/browser/extensions/formautofill/test/unit/test_clearPopulatedForm.js new file mode 100644 index 0000000000..db8ccb7621 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_clearPopulatedForm.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TESTCASES = [ + { + description: "Clear populated address form with text inputs", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + "given-name": "John", + "family-name": "Doe", + "street-addr": "1000 Main Street", + city: "Nowhere", + }, + expectedResult: { + "given-name": "", + "family-name": "", + "street-addr": "", + city: "", + }, + }, + { + description: "Clear populated address form with select and text inputs", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + "given-name": "John", + "family-name": "Doe", + "street-addr": "1000 Main Street", + state: "OH", + }, + expectedResult: { + "given-name": "", + "family-name": "", + "street-addr": "", + state: "AL", + }, + }, + { + description: + "Clear populated address form with select element with selected attribute and text inputs", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + "given-name": "John", + "family-name": "Doe", + "street-addr": "1000 Main Street", + state: "OH", + }, + expectedResult: { + "given-name": "", + "family-name": "", + "street-addr": "", + state: "AK", + }, + }, +]; + +add_task(async function do_test() { + let { FormAutofillHandler } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHandler.sys.mjs" + ); + for (let test of TESTCASES) { + info("Test case: " + test.description); + let testDoc = MockDocument.createTestDocument( + "http://localhost:8080/test", + test.document + ); + let form = testDoc.querySelector("form"); + let formLike = FormLikeFactory.createFromForm(form); + let handler = new FormAutofillHandler(formLike); + handler.collectFormFields(); + let focusedInput = testDoc.getElementById(test.focusedInputId); + handler.focusedInput = focusedInput; + let [adaptedProfile] = handler.activeSection.getAdaptedProfiles([ + test.profileData, + ]); + await handler.autofillFormFields(adaptedProfile, focusedInput); + + handler.activeSection.clearPopulatedForm(); + handler.activeSection.fieldDetails.forEach(detail => { + let element = detail.elementWeakRef.get(); + let id = element.id; + Assert.equal( + element.value, + test.expectedResult[id], + `Check the ${id} field was restored to the correct value` + ); + }); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_collectFormFields.js b/browser/extensions/formautofill/test/unit/test_collectFormFields.js new file mode 100644 index 0000000000..ac56d29c69 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js @@ -0,0 +1,638 @@ +/* + * Test for form auto fill content helper collectFormFields functions. + */ + +"use strict"; + +var FormAutofillHandler; +add_setup(async () => { + ({ FormAutofillHandler } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHandler.sys.mjs" + )); +}); + +const TESTCASES = [ + { + description: "Form without autocomplete property", + document: `
    + + + + + + + +
    `, + sections: [ + [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "address-line1" }, + { fieldName: "address-level2" }, + { fieldName: "country" }, + { fieldName: "email" }, + { fieldName: "tel" }, + ], + ], + ids: [ + "given-name", + "family-name", + "street-addr", + "city", + "country", + "email", + "phone", + ], + }, + { + description: + "An address and credit card form with autocomplete properties and 1 token", + document: `
    + + + + + + + + + + + +
    `, + sections: [ + [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "street-address" }, + { fieldName: "address-level2" }, + { fieldName: "country" }, + { fieldName: "email" }, + { fieldName: "tel" }, + ], + [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + ], + }, + { + description: "An address form with autocomplete properties and 2 tokens", + document: `
    + + + + + +
    `, + sections: [ + [ + { addressType: "shipping", fieldName: "given-name" }, + { addressType: "shipping", fieldName: "family-name" }, + { addressType: "shipping", fieldName: "street-address" }, + { addressType: "shipping", fieldName: "address-level2" }, + { addressType: "shipping", fieldName: "country" }, + { addressType: "shipping", fieldName: "email" }, + { addressType: "shipping", fieldName: "tel" }, + ], + ], + }, + { + description: + "Form with autocomplete properties and profile is partly matched", + document: `
    + + + + + +
    `, + sections: [ + [ + { addressType: "shipping", fieldName: "given-name" }, + { addressType: "shipping", fieldName: "family-name" }, + { addressType: "shipping", fieldName: "street-address" }, + { addressType: "shipping", fieldName: "address-level2" }, + { addressType: "shipping", fieldName: "country" }, + { addressType: "shipping", fieldName: "email" }, + { addressType: "shipping", fieldName: "tel" }, + ], + ], + }, + { + description: "It's a valid address and credit card form.", + document: `
    + + + + +
    `, + sections: [ + [ + { addressType: "shipping", fieldName: "given-name" }, + { addressType: "shipping", fieldName: "family-name" }, + { addressType: "shipping", fieldName: "street-address" }, + ], + [{ addressType: "shipping", fieldName: "cc-number" }], + ], + }, + { + description: "An invalid address form due to less than 3 fields.", + document: `
    + + +
    `, + sections: [], + }, + /* + * Valid Credit Card Form with autocomplete attribute + */ + { + description: "@autocomplete - A valid credit card form", + document: `
    + + + +
    `, + sections: [ + [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp" }, + ], + ], + }, + { + description: "@autocomplete - A valid credit card form without cc-numner", + document: `
    + + +
    `, + sections: [[{ fieldName: "cc-name" }, { fieldName: "cc-exp" }]], + }, + { + description: "@autocomplete - A valid cc-number only form", + document: `
    `, + sections: [[{ fieldName: "cc-number" }]], + }, + { + description: "@autocomplete - A valid cc-name only form", + document: `
    `, + sections: [[{ fieldName: "cc-name" }]], + }, + { + description: "@autocomplete - A valid cc-exp only form", + document: `
    `, + sections: [[{ fieldName: "cc-exp" }]], + }, + { + description: "@autocomplete - A valid cc-exp-month + cc-exp-year form", + document: `
    + + +
    `, + sections: [[{ fieldName: "cc-exp-month" }, { fieldName: "cc-exp-year" }]], + }, + { + description: "@autocomplete - A valid cc-exp-month only form", + document: `
    `, + sections: [[{ fieldName: "cc-exp-month" }]], + }, + { + description: "@autocomplete - A valid cc-exp-year only form", + document: `
    `, + sections: [[{ fieldName: "cc-exp-year" }]], + }, + /* + * Valid Credit Card Form when cc-number or cc-name is detected by fathom + */ + { + description: + "A valid credit card form without autocomplete attribute (cc-number is detected by fathom)", + document: `
    + + + +
    `, + sections: [ + [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp" }, + ], + ], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.8", + ], + ], + }, + { + description: + "A valid credit card form without autocomplete attribute (only cc-number is detected by fathom)", + document: `
    + + + +
    `, + sections: [ + [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp" }, + ], + ], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.8", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.types", + "cc-number", + ], + ], + }, + { + description: + "A valid credit card form without autocomplete attribute (only cc-name is detected by fathom)", + document: `
    + + +
    `, + sections: [[{ fieldName: "cc-name" }, { fieldName: "cc-exp" }]], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.8", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.types", + "cc-name", + ], + ], + }, + /* + * Invalid Credit Card Form when a cc-number or cc-name is detected by fathom + */ + { + description: + "A credit card form is invalid when a fathom detected cc-number field is the only field in the form", + document: `
    `, + sections: [], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.9", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.8", + ], + ], + }, + { + description: + "A credit card form is invalid when a fathom detected cc-name field is the only field in the form", + document: `
    `, + sections: [], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.9", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.8", + ], + ], + }, + /* + * Valid Credit Card Form when a cc-number or cc-name only form is detected by fathom (field is high confidence) + */ + { + description: + "A cc-number only form is considered a valid credit card form when fathom is confident and there is no other in the form", + document: `
    `, + sections: [[{ fieldName: "cc-number" }]], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.95", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.99", + ], + ], + }, + { + description: + "A cc-name only form is considered a valid credit card form when fathom is confident and there is no other in the form", + document: `
    `, + sections: [[{ fieldName: "cc-name" }]], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.95", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.99", + ], + ], + }, + /* + * Invalid Credit Card Form when none of the fields is identified by fathom + */ + { + description: + "A credit card form is invalid when none of the fields are identified by fathom or autocomplete", + document: `
    + + + +
    `, + sections: [], + prefs: [ + ["extensions.formautofill.creditCards.heuristics.fathom.types", ""], + ], + }, + // Special Cases + { + description: + "A credit card form with a high-confidence cc-name field is still considered invalid when there is another field", + document: `
    + + +
    `, + sections: [], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.95", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.96", + ], + ], + }, + { + description: "A valid credit card form with multiple cc-number fields", + document: `
    + + + + + + +
    `, + sections: [ + [ + { fieldName: "cc-number" }, + { fieldName: "cc-number" }, + { fieldName: "cc-number" }, + { fieldName: "cc-number" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + ], + ids: [ + "cc-number1", + "cc-number2", + "cc-number3", + "cc-number4", + "cc-exp-month", + "cc-exp-year", + ], + }, + { + description: "Three sets of adjacent phone number fields", + document: `
    + + + + + + + + + + + + + +
    `, + sections: [ + [ + { fieldName: "tel-area-code" }, + { fieldName: "tel-local-prefix" }, + { fieldName: "tel-local-suffix" }, + { fieldName: "tel-extension" }, + ], + [ + { fieldName: "tel-area-code" }, + { fieldName: "tel-local-prefix" }, + { fieldName: "tel-local-suffix" }, + + // TODO Bug 1421181 - "tel-country-code" field should belong to the next + // section. There should be a way to group the related fields during the + // parsing stage. + { fieldName: "tel-country-code" }, + ], + [ + { fieldName: "tel-area-code" }, + { fieldName: "tel-local-prefix" }, + { fieldName: "tel-local-suffix" }, + ], + ], + ids: [ + "shippingAC", + "shippingPrefix", + "shippingSuffix", + "shippingTelExt", + "billingAC", + "billingPrefix", + "billingSuffix", + "otherCC", + "otherAC", + "otherPrefix", + "otherSuffix", + ], + }, + { + description: + "Do not dedup the same field names of the different telephone fields.", + document: `
    + + + + + + + + +
    `, + sections: [ + [ + { fieldName: "given-name" }, + { fieldName: "family-name" }, + { fieldName: "street-address" }, + { fieldName: "email" }, + { fieldName: "tel" }, + { fieldName: "tel" }, + { fieldName: "tel" }, + ], + ], + ids: ["i1", "i2", "i3", "i4", "homePhone", "mobilePhone", "officePhone"], + }, + { + description: + "The duplicated phones of a single one and a set with ac, prefix, suffix.", + document: `
    + + + + + + + + +
    `, + sections: [ + [ + { addressType: "shipping", fieldName: "given-name" }, + { addressType: "shipping", fieldName: "family-name" }, + { addressType: "shipping", fieldName: "street-address" }, + { addressType: "shipping", fieldName: "email" }, + + // NOTES: Ideally, there is only one full telephone field(s) in a form for + // this case. We can see if there is any better solution later. + { addressType: "shipping", fieldName: "tel" }, + { addressType: "shipping", fieldName: "tel-area-code" }, + { addressType: "shipping", fieldName: "tel-local-prefix" }, + { addressType: "shipping", fieldName: "tel-local-suffix" }, + ], + ], + ids: [ + "i1", + "i2", + "i3", + "i4", + "singlePhone", + "shippingAreaCode", + "shippingPrefix", + "shippingSuffix", + ], + }, + { + description: "Always adopt the info from autocomplete attribute.", + document: `
    + + + + + +
    `, + sections: [ + [ + { addressType: "shipping", fieldName: "given-name" }, + { addressType: "shipping", fieldName: "family-name" }, + { addressType: "shipping", fieldName: "tel" }, + { addressType: "shipping", fieldName: "tel" }, + { addressType: "shipping", fieldName: "tel" }, + ], + ], + ids: [ + "given-name", + "family-name", + "dummyAreaCode", + "dummyPrefix", + "dummySuffix", + ], + }, +]; + +function verifyDetails(handlerDetails, testCaseDetails) { + if (handlerDetails === null) { + Assert.equal(handlerDetails, testCaseDetails); + return; + } + Assert.equal(handlerDetails.length, testCaseDetails.length, "field count"); + handlerDetails.forEach((detail, index) => { + Assert.equal( + detail.fieldName, + testCaseDetails[index].fieldName, + "fieldName" + ); + Assert.equal( + detail.section, + testCaseDetails[index].section ?? "", + "section" + ); + Assert.equal( + detail.addressType, + testCaseDetails[index].addressType ?? "", + "addressType" + ); + Assert.equal( + detail.contactType, + testCaseDetails[index].contactType ?? "", + "contactType" + ); + Assert.equal( + detail.elementWeakRef.get(), + testCaseDetails[index].elementWeakRef.get(), + "DOM reference" + ); + }); +} + +for (let tc of TESTCASES) { + (function () { + let testcase = tc; + add_task(async function () { + info("Starting testcase: " + testcase.description); + + if (testcase.prefs) { + testcase.prefs.forEach(pref => SetPref(pref[0], pref[1])); + } + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + testcase.sections.flat().forEach((field, idx) => { + let elementRef = doc.getElementById( + testcase.ids?.[idx] ?? field.fieldName + ); + field.elementWeakRef = Cu.getWeakReference(elementRef); + }); + + let form = doc.querySelector("form"); + let formLike = FormLikeFactory.createFromForm(form); + + let handler = new FormAutofillHandler(formLike); + let validFieldDetails = handler.collectFormFields(); + + Assert.equal( + handler.sections.length, + testcase.sections.length, + "section count" + ); + for (let i = 0; i < handler.sections.length; i++) { + let section = handler.sections[i]; + verifyDetails(section.fieldDetails, testcase.sections[i]); + } + verifyDetails(validFieldDetails, testcase.sections.flat()); + + if (testcase.prefs) { + testcase.prefs.forEach(pref => Services.prefs.clearUserPref(pref[0])); + } + }); + })(); +} diff --git a/browser/extensions/formautofill/test/unit/test_createRecords.js b/browser/extensions/formautofill/test/unit/test_createRecords.js new file mode 100644 index 0000000000..3d028e808e --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_createRecords.js @@ -0,0 +1,525 @@ +/* + * Test for the normalization of records created by FormAutofillHandler. + */ + +"use strict"; + +var FormAutofillHandler; +add_task(async function seutp() { + ({ FormAutofillHandler } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHandler.sys.mjs" + )); +}); + +const TESTCASES = [ + { + description: + "Don't contain a field whose length of value is greater than 200", + document: `
    + + + + + + +
    `, + formValue: { + "given-name": "John", + organization: "*".repeat(200), + "address-level1": "*".repeat(201), + country: "US", + "cc-number": "1111222233334444", + "cc-name": "*".repeat(201), + }, + expectedRecord: { + address: [ + { + "given-name": "John", + organization: "*".repeat(200), + "address-level1": "", + country: "US", + }, + ], + creditCard: [ + { + "cc-number": "1111222233334444", + "cc-name": "", + }, + ], + }, + }, + { + description: "Don't create address record if filled data is less than 3", + document: `
    + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + }, + expectedRecord: { + address: [], + creditCard: [], + }, + }, + { + description: `"country" using @autocomplete shouldn't be identified aggressively`, + document: `
    + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + country: "United States", + }, + expectedRecord: { + // "United States" is not a valid country, only country-name. See isRecordCreatable. + address: [], + creditCard: [], + }, + }, + { + description: `"country" using heuristics should be identified aggressively`, + document: `
    + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + country: "United States", + }, + expectedRecord: { + address: [ + { + "given-name": "John", + organization: "Mozilla", + country: "US", + }, + ], + creditCard: [], + }, + }, + { + description: `"tel" related fields should be concatenated`, + document: `
    + + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + "tel-country-code": "+1", + "tel-national": "1234567890", + }, + expectedRecord: { + address: [ + { + "given-name": "John", + organization: "Mozilla", + tel: "+11234567890", + }, + ], + creditCard: [], + }, + }, + { + description: `"tel" should be removed if it's too short`, + document: `
    + + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + country: "US", + tel: "1234", + }, + expectedRecord: { + address: [ + { + "given-name": "John", + organization: "Mozilla", + country: "US", + tel: "", + }, + ], + creditCard: [], + }, + }, + { + description: `"tel" should be removed if it's too long`, + document: `
    + + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + country: "US", + tel: "1234567890123456", + }, + expectedRecord: { + address: [ + { + "given-name": "John", + organization: "Mozilla", + country: "US", + tel: "", + }, + ], + creditCard: [], + }, + }, + { + description: `"tel" should be removed if it contains invalid characters`, + document: `
    + + + + +
    `, + formValue: { + "given-name": "John", + organization: "Mozilla", + country: "US", + tel: "12345###!!!", + }, + expectedRecord: { + address: [ + { + "given-name": "John", + organization: "Mozilla", + country: "US", + tel: "", + }, + ], + creditCard: [], + }, + }, + { + description: "All name related fields should be counted as 1 field only.", + document: `
    + + + +
    `, + formValue: { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + }, + expectedRecord: { + address: [], + creditCard: [], + }, + }, + { + description: + "All telephone related fields should be counted as 1 field only.", + document: `
    + + + + +
    `, + formValue: { + "tel-country-code": "+1", + "tel-area-code": "123", + "tel-local": "4567890", + organization: "Mozilla", + }, + expectedRecord: { + address: [], + creditCard: [], + }, + }, + { + description: + "A credit card form with the value of cc-number, cc-exp, cc-name and cc-type.", + document: `
    + + + + +
    `, + formValue: { + "cc-number": "5105105105105100", + "cc-name": "Foo Bar", + "cc-exp": "2022-06", + "cc-type": "Visa", + }, + expectedRecord: { + address: [], + creditCard: [ + { + "cc-number": "5105105105105100", + "cc-name": "Foo Bar", + "cc-exp": "2022-06", + "cc-type": "Visa", + "cc-exp-month": "6", + "cc-exp-year": "2022", + }, + ], + }, + }, + { + description: "A credit card form with cc-number value only.", + document: `
    + +
    `, + formValue: { + "cc-number": "4111111111111111", + }, + expectedRecord: { + address: [], + creditCard: [ + { + "cc-number": "4111111111111111", + }, + ], + }, + }, + { + description: "A credit card form must have cc-number value.", + document: `
    + + + +
    `, + formValue: { + "cc-number": "", + "cc-name": "Foo Bar", + "cc-exp": "2022-06", + }, + expectedRecord: { + address: [], + creditCard: [], + }, + }, + { + description: "A credit card form must have cc-number field.", + document: `
    + + +
    `, + formValue: { + "cc-name": "Foo Bar", + "cc-exp": "2022-06", + }, + expectedRecord: { + address: [], + creditCard: [], + }, + }, + { + description: "A form with multiple sections", + document: `
    + + + + + + + + + + + + + + + + + + + +
    `, + formValue: { + "given-name": "Bar", + organization: "Foo", + country: "US", + + "given-name-shipping": "John", + "family-name-shipping": "Doe", + "organization-shipping": "Mozilla", + "country-shipping": "US", + + "given-name-billing": "Foo", + "organization-billing": "Bar", + "country-billing": "US", + + "cc-number-section-one": "4111111111111111", + "cc-name-section-one": "John", + + "cc-number-section-two": "5105105105105100", + "cc-name-section-two": "Foo Bar", + "cc-exp-section-two": "2026-26", + }, + expectedRecord: { + address: [ + { + "given-name": "Bar", + organization: "Foo", + country: "US", + }, + { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + country: "US", + }, + { + "given-name": "Foo", + organization: "Bar", + country: "US", + }, + ], + creditCard: [ + { + "cc-number": "4111111111111111", + "cc-name": "John", + }, + { + "cc-number": "5105105105105100", + "cc-name": "Foo Bar", + "cc-exp": "2026-26", + }, + ], + }, + }, + { + description: "A credit card form with a cc-type select.", + document: `
    + + + +
    `, + formValue: { + "cc-number": "5105105105105100", + }, + expectedRecord: { + address: [], + creditCard: [ + { + "cc-number": "5105105105105100", + "cc-type": "visa", + }, + ], + }, + }, + { + description: "A credit card form with a cc-type select from label.", + document: `
    + + + +
    `, + formValue: { + "cc-number": "5105105105105100", + "cc-type": "A", + }, + expectedRecord: { + address: [], + creditCard: [ + { + "cc-number": "5105105105105100", + "cc-type": "amex", + }, + ], + }, + }, + { + description: + "A credit card form with separate expiry fields should have normalized expiry data.", + document: `
    + + + +
    `, + formValue: { + "cc-number": "5105105105105100", + "cc-exp-month": "05", + "cc-exp-year": "26", + }, + expectedRecord: { + address: [], + creditCard: [ + { + "cc-number": "5105105105105100", + "cc-exp-month": "5", + "cc-exp-year": "2026", + }, + ], + }, + }, + { + description: + "A credit card form with combined expiry fields should have normalized expiry data.", + document: `
    + + +
    `, + formValue: { + "cc-number": "5105105105105100", + "cc-exp": "07/27", + }, + expectedRecord: { + address: [], + creditCard: [ + { + "cc-number": "5105105105105100", + "cc-exp": "07/27", + "cc-exp-month": "7", + "cc-exp-year": "2027", + }, + ], + }, + }, +]; + +for (let testcase of TESTCASES) { + add_task(async function () { + info("Starting testcase: " + testcase.description); + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + let form = doc.querySelector("form"); + let formLike = FormLikeFactory.createFromForm(form); + let handler = new FormAutofillHandler(formLike); + + handler.collectFormFields(); + + for (let id in testcase.formValue) { + doc.getElementById(id).value = testcase.formValue[id]; + } + + let record = handler.createRecords(); + + let expectedRecord = testcase.expectedRecord; + for (let type in record) { + Assert.deepEqual( + record[type].map(secRecord => secRecord.record), + expectedRecord[type] + ); + } + }); +} diff --git a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js new file mode 100644 index 0000000000..3011fe885f --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js @@ -0,0 +1,926 @@ +/** + * Tests FormAutofillStorage object with creditCards records. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); +const { CreditCard } = ChromeUtils.importESModule( + "resource://gre/modules/CreditCard.sys.mjs" +); + +let FormAutofillStorage; +let CREDIT_CARD_SCHEMA_VERSION; +add_setup(async () => { + ({ FormAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + )); + ({ CREDIT_CARD_SCHEMA_VERSION } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorageBase.sys.mjs" + )); +}); + +const TEST_STORE_FILE_NAME = "test-credit-card.json"; +const COLLECTION_NAME = "creditCards"; + +const TEST_CREDIT_CARD_1 = { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, +}; + +const TEST_CREDIT_CARD_2 = { + "cc-name": "Timothy Berners-Lee", + "cc-number": "5103059495477870", + "cc-exp-month": 12, + "cc-exp-year": 2022, +}; + +const TEST_CREDIT_CARD_3 = { + "cc-number": "3589993783099582", + "cc-exp-month": 1, + "cc-exp-year": 2000, +}; + +const TEST_CREDIT_CARD_4 = { + "cc-name": "Foo Bar", + "cc-number": "3589993783099582", +}; + +const TEST_CREDIT_CARD_WITH_BILLING_ADDRESS = { + "cc-name": "J. Smith", + "cc-number": "4111111111111111", + billingAddressGUID: "9m6hf4gfr6ge", +}; + +const TEST_CREDIT_CARD_WITH_EMPTY_FIELD = { + billingAddressGUID: "", + "cc-name": "", + "cc-number": "344060747836806", + "cc-exp-month": 1, +}; + +const TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD = { + "cc-given-name": "", + "cc-additional-name": "", + "cc-family-name": "", + "cc-exp": "", + "cc-number": "5415425865751454", +}; + +const TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR = { + "cc-number": "344060747836806", + "cc-exp-month": 1, + "cc-exp-year": 12, +}; + +const TEST_CREDIT_CARD_WITH_INVALID_FIELD = { + "cc-name": "John Doe", + "cc-number": "344060747836806", + "cc-type": { invalid: "invalid" }, +}; + +const TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE = { + "cc-name": "John Doe", + "cc-number": "5103059495477870", + "cc-exp-month": 13, + "cc-exp-year": -3, +}; + +const TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS = { + "cc-name": "John Doe", + "cc-number": "5103 0594 9547 7870", +}; + +const TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE = { + "cc-exp-month": 13, +}; + +const TEST_CREDIT_CARD_EMPTY_AFTER_UPDATE_CREDIT_CARD_1 = { + "cc-name": "", + "cc-number": "", + "cc-exp-month": 13, + "cc-exp-year": "", +}; + +const MERGE_TESTCASES = [ + { + description: "Merge a superset", + creditCardInStorage: { + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + "unknown-1": "an unknown field from another client", + }, + creditCardToMerge: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + "unknown-1": "an unknown field from another client", + }, + expectedCreditCard: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + "unknown-1": "an unknown field from another client", + }, + }, + { + description: "Merge a superset with billingAddressGUID", + creditCardInStorage: { + "cc-number": "4929001587121045", + }, + creditCardToMerge: { + "cc-number": "4929001587121045", + billingAddressGUID: "ijsnbhfr", + }, + expectedCreditCard: { + "cc-number": "4929001587121045", + billingAddressGUID: "ijsnbhfr", + }, + }, + { + description: "Merge a subset", + creditCardInStorage: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }, + creditCardToMerge: { + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }, + expectedCreditCard: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }, + noNeedToUpdate: true, + }, + { + description: "Merge a subset with billingAddressGUID", + creditCardInStorage: { + "cc-number": "4929001587121045", + billingAddressGUID: "8fhdb3ug6", + }, + creditCardToMerge: { + "cc-number": "4929001587121045", + }, + expectedCreditCard: { + billingAddressGUID: "8fhdb3ug6", + "cc-number": "4929001587121045", + }, + noNeedToUpdate: true, + }, + { + description: "Merge an creditCard with partial overlaps", + creditCardInStorage: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + creditCardToMerge: { + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }, + expectedCreditCard: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }, + }, +]; + +let prepareTestCreditCards = async function (path) { + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "add" && + subject.wrappedJSObject.guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + Assert.ok(await profileStorage.creditCards.add(TEST_CREDIT_CARD_1)); + await onChanged; + Assert.ok(await profileStorage.creditCards.add(TEST_CREDIT_CARD_2)); + await onChanged; + await profileStorage._saveImmediately(); +}; + +let reCCNumber = /^(\*+)(.{4})$/; + +let do_check_credit_card_matches = (creditCardWithMeta, creditCard) => { + for (let key in creditCard) { + if (key == "cc-number") { + let matches = reCCNumber.exec(creditCardWithMeta["cc-number"]); + Assert.notEqual(matches, null); + Assert.equal( + creditCardWithMeta["cc-number"].length, + creditCard["cc-number"].length + ); + Assert.equal(creditCard["cc-number"].endsWith(matches[2]), true); + Assert.notEqual(creditCard["cc-number-encrypted"], ""); + } else { + Assert.equal(creditCardWithMeta[key], creditCard[key], "Testing " + key); + } + } +}; + +add_task(async function test_initialize() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + Assert.equal(profileStorage._store.data.version, 1); + Assert.equal(profileStorage._store.data.creditCards.length, 0); + + let data = profileStorage._store.data; + Assert.deepEqual(data.creditCards, []); + + await profileStorage._saveImmediately(); + + profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + Assert.deepEqual(profileStorage._store.data, data); +}); + +add_task(async function test_getAll() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + await prepareTestCreditCards(path); + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + + Assert.equal(creditCards.length, 2); + do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1); + do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2); + + // Check computed fields. + Assert.equal(creditCards[0]["cc-given-name"], "John"); + Assert.equal(creditCards[0]["cc-family-name"], "Doe"); + Assert.equal(creditCards[0]["cc-exp"], "2017-04"); + + // Test with rawData set. + creditCards = await profileStorage.creditCards.getAll({ rawData: true }); + Assert.equal(creditCards[0]["cc-given-name"], undefined); + Assert.equal(creditCards[0]["cc-family-name"], undefined); + Assert.equal(creditCards[0]["cc-exp"], undefined); + + // Modifying output shouldn't affect the storage. + creditCards[0]["cc-name"] = "test"; + do_check_credit_card_matches( + (await profileStorage.creditCards.getAll())[0], + TEST_CREDIT_CARD_1 + ); +}); + +add_task(async function test_get() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + await prepareTestCreditCards(path); + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + let guid = creditCards[0].guid; + + let creditCard = await profileStorage.creditCards.get(guid); + do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_1); + + // Modifying output shouldn't affect the storage. + creditCards[0]["cc-name"] = "test"; + do_check_credit_card_matches( + await profileStorage.creditCards.get(guid), + TEST_CREDIT_CARD_1 + ); + + Assert.equal(await profileStorage.creditCards.get("INVALID_GUID"), null); +}); + +add_task(async function test_add() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + await prepareTestCreditCards(path); + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + + Assert.equal(creditCards.length, 2); + + do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1); + do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2); + + Assert.notEqual(creditCards[0].guid, undefined); + Assert.equal(creditCards[0].version, CREDIT_CARD_SCHEMA_VERSION); + Assert.notEqual(creditCards[0].timeCreated, undefined); + Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated); + Assert.equal(creditCards[0].timeLastUsed, 0); + Assert.equal(creditCards[0].timesUsed, 0); + + // Empty string should be deleted before saving. + await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD); + let creditCard = profileStorage.creditCards._data[2]; + Assert.equal( + creditCard["cc-exp-month"], + TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"] + ); + Assert.equal(creditCard["cc-name"], undefined); + Assert.equal(creditCard.billingAddressGUID, undefined); + + // Empty computed fields shouldn't cause any problem. + await profileStorage.creditCards.add( + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD + ); + creditCard = profileStorage.creditCards._data[3]; + Assert.equal( + creditCard["cc-number"], + CreditCard.getLongMaskedNumber( + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"] + ) + ); + + await Assert.rejects( + profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_FIELD), + /"cc-type" contains invalid data type: object/ + ); + + await Assert.rejects( + profileStorage.creditCards.add({}), + /Record contains no valid field\./ + ); + + await Assert.rejects( + profileStorage.creditCards.add(TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE), + /Record contains no valid field\./ + ); +}); + +add_task(async function test_addWithBillingAddress() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + + Assert.equal(creditCards.length, 0); + + await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_BILLING_ADDRESS); + + creditCards = await profileStorage.creditCards.getAll(); + Assert.equal(creditCards.length, 1); + do_check_credit_card_matches( + creditCards[0], + TEST_CREDIT_CARD_WITH_BILLING_ADDRESS + ); +}); + +add_task(async function test_update() { + // Test assumes that when an entry is saved a second time, it's last modified date will + // be different from the first. With high values of precision reduction, we execute too + // fast for that to be true. + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function () { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + let path = getTempFile(TEST_STORE_FILE_NAME).path; + await prepareTestCreditCards(path); + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + let guid = creditCards[1].guid; + let timeLastModified = creditCards[1].timeLastModified; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "update" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + + Assert.notEqual(creditCards[1]["cc-name"], undefined); + await profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_3); + await onChanged; + await profileStorage._saveImmediately(); + + profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCard = await profileStorage.creditCards.get(guid); + + Assert.equal(creditCard["cc-name"], undefined); + Assert.notEqual(creditCard.timeLastModified, timeLastModified); + do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_3); + + // Empty string should be deleted while updating. + await profileStorage.creditCards.update( + profileStorage.creditCards._data[0].guid, + TEST_CREDIT_CARD_WITH_EMPTY_FIELD + ); + creditCard = profileStorage.creditCards._data[0]; + Assert.equal( + creditCard["cc-exp-month"], + TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"] + ); + Assert.equal(creditCard["cc-name"], undefined); + Assert.equal(creditCard["cc-type"], "amex"); + Assert.equal(creditCard.billingAddressGUID, undefined); + + // Empty computed fields shouldn't cause any problem. + await profileStorage.creditCards.update( + profileStorage.creditCards._data[0].guid, + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, + false + ); + creditCard = profileStorage.creditCards._data[0]; + Assert.equal( + creditCard["cc-number"], + CreditCard.getLongMaskedNumber( + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"] + ) + ); + await profileStorage.creditCards.update( + profileStorage.creditCards._data[1].guid, + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, + true + ); + creditCard = profileStorage.creditCards._data[1]; + Assert.equal( + creditCard["cc-number"], + CreditCard.getLongMaskedNumber( + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"] + ) + ); + + // Decryption failure of existing record should not prevent it from being updated. + creditCard = profileStorage.creditCards._data[0]; + creditCard["cc-number-encrypted"] = "INVALID"; + await profileStorage.creditCards.update( + profileStorage.creditCards._data[0].guid, + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, + false + ); + creditCard = profileStorage.creditCards._data[0]; + Assert.equal( + creditCard["cc-number"], + CreditCard.getLongMaskedNumber( + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"] + ) + ); + + await Assert.rejects( + profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3), + /No matching record\./ + ); + + await Assert.rejects( + profileStorage.creditCards.update( + guid, + TEST_CREDIT_CARD_WITH_INVALID_FIELD + ), + /"cc-type" contains invalid data type: object/ + ); + + await Assert.rejects( + profileStorage.creditCards.update(guid, {}), + /Record contains no valid field\./ + ); + + await Assert.rejects( + profileStorage.creditCards.update( + guid, + TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE + ), + /Record contains no valid field\./ + ); + + await profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_1); + await Assert.rejects( + profileStorage.creditCards.update( + guid, + TEST_CREDIT_CARD_EMPTY_AFTER_UPDATE_CREDIT_CARD_1 + ), + /Record contains no valid field\./ + ); +}); + +add_task(async function test_validate() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + await profileStorage.creditCards.add( + TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE + ); + await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR); + await profileStorage.creditCards.add( + TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS + ); + let creditCards = await profileStorage.creditCards.getAll(); + + Assert.equal(creditCards[0]["cc-exp-month"], undefined); + Assert.equal(creditCards[0]["cc-exp-year"], undefined); + Assert.equal(creditCards[0]["cc-exp"], undefined); + + let month = TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-month"]; + let year = + parseInt(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-year"], 10) + 2000; + Assert.equal(creditCards[1]["cc-exp-month"], month); + Assert.equal(creditCards[1]["cc-exp-year"], year); + Assert.equal( + creditCards[1]["cc-exp"], + year + "-" + month.toString().padStart(2, "0") + ); + + Assert.equal(creditCards[2]["cc-number"].length, 16); +}); + +add_task(async function test_notifyUsed() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + await prepareTestCreditCards(path); + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + let guid = creditCards[1].guid; + let timeLastUsed = creditCards[1].timeLastUsed; + let timesUsed = creditCards[1].timesUsed; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "notifyUsed" && + subject.wrappedJSObject.collectionName == COLLECTION_NAME && + subject.wrappedJSObject.guid == guid + ); + + profileStorage.creditCards.notifyUsed(guid); + await onChanged; + await profileStorage._saveImmediately(); + + profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCard = await profileStorage.creditCards.get(guid); + + Assert.equal(creditCard.timesUsed, timesUsed + 1); + Assert.notEqual(creditCard.timeLastUsed, timeLastUsed); + + Assert.throws( + () => profileStorage.creditCards.notifyUsed("INVALID_GUID"), + /No matching record\./ + ); +}); + +add_task(async function test_remove() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + await prepareTestCreditCards(path); + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let creditCards = await profileStorage.creditCards.getAll(); + let guid = creditCards[1].guid; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "remove" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + + Assert.equal(creditCards.length, 2); + + profileStorage.creditCards.remove(guid); + await onChanged; + await profileStorage._saveImmediately(); + + profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + creditCards = await profileStorage.creditCards.getAll(); + + Assert.equal(creditCards.length, 1); + + Assert.equal(await profileStorage.creditCards.get(guid), null); +}); + +MERGE_TESTCASES.forEach(testcase => { + add_task(async function test_merge() { + info("Starting testcase: " + testcase.description); + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + [testcase.creditCardInStorage], + "creditCards" + ); + let creditCards = await profileStorage.creditCards.getAll(); + let guid = creditCards[0].guid; + let timeLastModified = creditCards[0].timeLastModified; + // Merge creditCard and verify the guid in notifyObservers subject + let onMerged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "update" && + subject.wrappedJSObject.guid == guid && + subject.wrappedJSObject.collectionName == COLLECTION_NAME + ); + // Force to create sync metadata. + profileStorage.creditCards.pullSyncChanges(); + Assert.equal(getSyncChangeCounter(profileStorage.creditCards, guid), 1); + Assert.ok( + await profileStorage.creditCards.mergeIfPossible( + guid, + testcase.creditCardToMerge + ) + ); + if (!testcase.noNeedToUpdate) { + await onMerged; + } + creditCards = await profileStorage.creditCards.getAll(); + Assert.equal(creditCards.length, 1); + do_check_credit_card_matches(creditCards[0], testcase.expectedCreditCard); + if (!testcase.noNeedToUpdate) { + // Record merging should update timeLastModified and bump the change counter. + Assert.notEqual(creditCards[0].timeLastModified, timeLastModified); + Assert.equal(getSyncChangeCounter(profileStorage.creditCards, guid), 2); + } else { + // Subset record merging should not update timeLastModified and the change + // counter is still the same. + Assert.equal(creditCards[0].timeLastModified, timeLastModified); + Assert.equal(getSyncChangeCounter(profileStorage.creditCards, guid), 1); + } + }); +}); + +add_task(async function test_merge_unable_merge() { + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + [TEST_CREDIT_CARD_1], + "creditCards" + ); + + let creditCards = await profileStorage.creditCards.getAll(); + let guid = creditCards[0].guid; + // Force to create sync metadata. + profileStorage.creditCards.pullSyncChanges(); + Assert.equal(getSyncChangeCounter(profileStorage.creditCards, guid), 1); + + // Unable to merge because of conflict + let anotherCreditCard = profileStorage.creditCards._clone(TEST_CREDIT_CARD_1); + anotherCreditCard["cc-name"] = "Foo Bar"; + Assert.equal( + await profileStorage.creditCards.mergeIfPossible(guid, anotherCreditCard), + false + ); + // The change counter is unchanged. + Assert.equal(getSyncChangeCounter(profileStorage.creditCards, guid), 1); + + // Unable to merge because no credit card number + anotherCreditCard = profileStorage.creditCards._clone(TEST_CREDIT_CARD_1); + anotherCreditCard["cc-number"] = ""; + Assert.equal( + await profileStorage.creditCards.mergeIfPossible(guid, anotherCreditCard), + false + ); + // The change counter is still unchanged. + Assert.equal(getSyncChangeCounter(profileStorage.creditCards, guid), 1); +}); + +add_task(async function test_mergeToStorage() { + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + [TEST_CREDIT_CARD_3, TEST_CREDIT_CARD_4], + "creditCards" + ); + // Merge a creditCard to storage + let anotherCreditCard = profileStorage.creditCards._clone(TEST_CREDIT_CARD_3); + anotherCreditCard["cc-name"] = "Foo Bar"; + Assert.equal( + (await profileStorage.creditCards.mergeToStorage(anotherCreditCard)).length, + 2 + ); + Assert.equal( + (await profileStorage.creditCards.getAll())[0]["cc-name"], + "Foo Bar" + ); + Assert.equal( + (await profileStorage.creditCards.getAll())[0]["cc-exp"], + "2000-01" + ); + Assert.equal( + (await profileStorage.creditCards.getAll())[1]["cc-name"], + "Foo Bar" + ); + Assert.equal( + (await profileStorage.creditCards.getAll())[1]["cc-exp"], + "2000-01" + ); + + // Empty computed fields shouldn't cause any problem. + Assert.equal( + ( + await profileStorage.creditCards.mergeToStorage( + TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD + ) + ).length, + 0 + ); +}); + +add_task(async function test_getDuplicateRecords() { + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + [TEST_CREDIT_CARD_3], + "creditCards" + ); + let guid = profileStorage.creditCards._data[0].guid; + + // Absolutely a duplicate. + let getDuplicateRecords = + profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_3); + let dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // Absolutely not a duplicate. + getDuplicateRecords = + profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_1); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe, null); + + // Subset with the same number is a duplicate. + let record = Object.assign({}, TEST_CREDIT_CARD_3); + delete record["cc-exp-month"]; + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // Superset with the same number is a duplicate. + record = Object.assign({}, TEST_CREDIT_CARD_3); + record["cc-name"] = "John Doe"; + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // Numbers with the same last 4 digits shouldn't be treated as a duplicate. + record = Object.assign({}, TEST_CREDIT_CARD_3); + let last4Digits = record["cc-number"].substr(-4); + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // This number differs from TEST_CREDIT_CARD_3 by swapping the order of the + // 09 and 90 adjacent digits, which is still a valid credit card number. + record["cc-number"] = "358999378390" + last4Digits; + + // We don't treat numbers with the same last 4 digits as a duplicate. + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe, null); +}); + +add_task(async function test_getDuplicateRecordsMatch() { + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + [TEST_CREDIT_CARD_2], + "creditCards" + ); + let guid = profileStorage.creditCards._data[0].guid; + + // Absolutely a duplicate. + let getDuplicateRecords = + profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_2); + let dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // Absolutely not a duplicate. + getDuplicateRecords = + profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_1); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe, null); + + record = Object.assign({}, TEST_CREDIT_CARD_2); + + // We change month from `1` to `2` + record["cc-exp-month"] = 2; + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // We change year from `2000` to `2001` + record["cc-exp-year"] = 2001; + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); + + // New name, same card + record["cc-name"] = "John Doe"; + getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record); + dupe = (await getDuplicateRecords.next()).value; + Assert.equal(dupe.guid, guid); +}); + +add_task(async function test_getMatchRecord() { + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + [TEST_CREDIT_CARD_2], + "creditCards" + ); + let guid = profileStorage.creditCards._data[0].guid; + + const TEST_FIELDS = { + "cc-name": "John Doe", + "cc-exp-month": 10, + "cc-exp-year": 2001, + }; + + // Absolutely a match. + let getMatchRecords = + profileStorage.creditCards.getMatchRecords(TEST_CREDIT_CARD_2); + let match = (await getMatchRecords.next()).value; + Assert.equal(match.guid, guid); + + // Subset with the same number is a match. + for (const field of Object.keys(TEST_FIELDS)) { + let record = Object.assign({}, TEST_CREDIT_CARD_2); + delete record[field]; + getMatchRecords = profileStorage.creditCards.getMatchRecords(record); + match = (await getMatchRecords.next()).value; + Assert.equal(match.guid, guid); + } + + // Subset with different number is not a match. + for (const field of Object.keys(TEST_FIELDS)) { + let record = Object.assign({}, TEST_CREDIT_CARD_2, { + "cc-number": TEST_CREDIT_CARD_1["cc-number"], + }); + delete record[field]; + getMatchRecords = profileStorage.creditCards.getMatchRecords(record); + match = (await getMatchRecords.next()).value; + Assert.equal(match, null); + } + + // Superset with the same number is not a match. + for (const [field, value] of Object.entries(TEST_FIELDS)) { + let record = Object.assign({}, TEST_CREDIT_CARD_2); + record[field] = value; + getMatchRecords = profileStorage.creditCards.getMatchRecords(record); + match = (await getMatchRecords.next()).value; + Assert.equal(match, null); + } + + // Superset with different number is not a match. + for (const [field, value] of Object.entries(TEST_FIELDS)) { + let record = Object.assign({}, TEST_CREDIT_CARD_2, { + "cc-number": TEST_CREDIT_CARD_1["cc-number"], + }); + record[field] = value; + getMatchRecords = profileStorage.creditCards.getMatchRecords(record); + match = (await getMatchRecords.next()).value; + Assert.equal(match, null); + } +}); + +add_task(async function test_creditCardFillDisabled() { + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + false + ); + + let path = getTempFile(TEST_STORE_FILE_NAME).path; + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + Assert.equal( + !!profileStorage.creditCards, + true, + "credit card records initialized and available." + ); + + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + true + ); +}); diff --git a/browser/extensions/formautofill/test/unit/test_extractLabelStrings.js b/browser/extensions/formautofill/test/unit/test_extractLabelStrings.js new file mode 100644 index 0000000000..4c03c263f8 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_extractLabelStrings.js @@ -0,0 +1,77 @@ +"use strict"; + +var { LabelUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/LabelUtils.sys.mjs" +); + +const TESTCASES = [ + { + description: "A label element contains one input element.", + document: ``, + inputId: "typeA", + expectedStrings: ["label type A"], + }, + { + description: "A label element with inner div contains one input element.", + document: ``, + inputId: "typeB", + expectedStrings: ["label type B", "inner div"], + }, + { + description: + "A label element with inner prefix/postfix strings contains span elements.", + document: ``, + inputId: "typeC", + expectedStrings: [ + "label type C", + "inner div prefix", + "test C-1", + "test C-2", + "inner div postfix", + ], + }, +]; + +TESTCASES.forEach(testcase => { + add_task(async function () { + info("Starting testcase: " + testcase.description); + LabelUtils._labelStrings = new WeakMap(); + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + + let element = doc.getElementById(testcase.inputId); + let strings = LabelUtils.extractLabelStrings(element); + + Assert.deepEqual(strings, testcase.expectedStrings); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_findLabelElements.js b/browser/extensions/formautofill/test/unit/test_findLabelElements.js new file mode 100644 index 0000000000..956ace83f6 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_findLabelElements.js @@ -0,0 +1,100 @@ +"use strict"; + +var { LabelUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/LabelUtils.sys.mjs" +); + +const TESTCASES = [ + { + description: "Input contains in a label element.", + document: `
    + +
    `, + inputId: "typeA", + expectedLabelIds: ["labelA"], + }, + { + description: "Input contains in a label element.", + document: ``, + inputId: "typeB", + expectedLabelIds: ["labelB"], + }, + { + description: '"for" attribute used to indicate input by one label.', + document: ` + `, + inputId: "typeC", + expectedLabelIds: ["labelC"], + }, + { + description: '"for" attribute used to indicate input by multiple labels.', + document: `
    + + + + +
    `, + inputId: "typeD", + expectedLabelIds: ["labelD1", "labelD2", "labelD3"], + }, + { + description: + '"for" attribute used to indicate input by multiple labels with space prefix/postfix.', + document: ` + + + + `, + inputId: " typeE ", + expectedLabelIds: [], + }, + { + description: "Input contains in a label element.", + document: ` + `, + elementId: "targetElement", + expectedReturnValue: ["email", null, null], + }, + { + description: + "A label element is out of the form contains the related input", + document: ` +
    + +
    `, + elementId: "targetElement", + expectedReturnValue: ["email", null, null], + }, + { + description: "A label element contains span element", + document: ` +
    + +
    `, + elementId: "targetElement", + expectedReturnValue: ["email", null, null], + }, + { + description: "The signature in 'name' attr of an input", + document: ``, + elementId: "targetElement", + expectedReturnValue: ["email", null, null], + }, + { + description: "The signature in 'id' attr of an input", + document: ``, + elementId: "targetElement_email", + expectedReturnValue: ["email", null, null], + }, + { + description: "Select element in a label element", + document: `
    + +
    `, + elementId: "targetElement", + expectedReturnValue: ["address-level1", null, null], + }, + { + description: "A select element without a form wrapped", + document: ` + `, + elementId: "targetElement", + expectedReturnValue: ["address-level1", null, null], + }, + { + description: "address line input", + document: ` + `, + elementId: "targetElement", + expectedReturnValue: ["street-address", null, null], + }, + { + description: "CJK character - Traditional Chinese", + document: ``, + elementId: "targetElement", + expectedReturnValue: ["postal-code", null, null], + }, + { + description: "CJK character - Japanese", + document: ``, + elementId: "targetElement", + expectedReturnValue: ["postal-code", null, null], + }, + { + description: "CJK character - Korean", + document: ``, + elementId: "targetElement", + expectedReturnValue: ["postal-code", null, null], + }, + { + description: "", + document: ``, + elementId: "targetElement", + expectedReturnValue: ["name", null, null], + }, + { + description: 'input element with "submit" type', + document: ``, + elementId: "targetElement", + expectedReturnValue: [null, null, null], + }, + { + description: "The signature in 'name' attr of an email input", + document: ``, + elementId: "targetElement", + expectedReturnValue: ["email", null, null], + }, + { + description: 'input element with "email" type', + document: ``, + elementId: "targetElement", + expectedReturnValue: ["email", null, null], + }, + { + description: "Exclude United State string", + document: ``, + elementId: "targetElement", + expectedReturnValue: [null, null, null], + }, + { + description: '"County" field with "United State" string', + document: ``, + elementId: "targetElement", + expectedReturnValue: ["address-level1", null, null], + }, + { + description: '"city" field with double "United State" string', + document: ``, + elementId: "targetElement", + expectedReturnValue: ["address-level2", null, null], + }, + { + description: "Verify credit card number", + document: `
    + + +
    `, + elementId: "targetElement", + expectedReturnValue: ["cc-number", null, 1], + }, + { + description: "Identify credit card type field", + document: `
    + + +
    `, + elementId: "targetElement", + expectedReturnValue: ["cc-type", null, null], + }, + { + description: `Identify address field when contained in a form with autocomplete="off"`, + document: `
    + +
    `, + elementId: "given-name", + expectedReturnValue: ["given-name", null, null], + }, + { + description: `Identify address field that has a placeholder but no label associated with it`, + document: `
    + +
    `, + elementId: "targetElement", + expectedReturnValue: ["name", null, null], + }, + { + description: `Identify address field that has a placeholder, no associated label, and its autocomplete attribute is "off"`, + document: `
    + +
    `, + elementId: "targetElement", + expectedReturnValue: ["street-address", null, null], + }, + { + description: `Identify address field that has a placeholder, no associated label, and the form's autocomplete attribute is "off"`, + document: `
    + +
    `, + elementId: "targetElement", + expectedReturnValue: ["country", null, null], + }, +]; + +add_setup(async function () { + Services.prefs.setStringPref( + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "1" + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence" + ); + }); +}); + +TESTCASES.forEach(testcase => { + add_task(async function () { + info("Starting testcase: " + testcase.description); + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + + let element = doc.getElementById(testcase.elementId); + let value = FormAutofillHeuristics.inferFieldInfo(element); + + Assert.deepEqual(value, testcase.expectedReturnValue); + LabelUtils.clearLabelMap(); + }); +}); + +add_task(async function test_regexp_list() { + info("Verify the fieldName support for select element."); + let SUPPORT_LIST = { + email: null, // email + "tel-extension": null, // tel-extension + phone: null, // tel + organization: null, // organization + "street-address": null, // street-address + address1: null, // address-line1 + address2: null, // address-line2 + address3: null, // address-line3 + city: "address-level2", + region: "address-level1", + "postal-code": null, // postal-code + country: "country", + fullname: null, // name + fname: null, // given-name + mname: null, // additional-name + lname: null, // family-name + cardholder: null, // cc-name + "cc-number": null, // cc-number + addmonth: "cc-exp-month", + addyear: "cc-exp-year", + }; + for (let label of Object.keys(SUPPORT_LIST)) { + let testcase = { + description: `A select element supports ${label} or not`, + document: ``, + elementId: label, + expectedReturnValue: SUPPORT_LIST[label] + ? [SUPPORT_LIST[label], null, null] + : [null, null, null], + }; + info(testcase.description); + info(testcase.document); + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + + let element = doc.getElementById(testcase.elementId); + let value = FormAutofillHeuristics.inferFieldInfo(element); + + Assert.deepEqual(value, testcase.expectedReturnValue, label); + } + LabelUtils.clearLabelMap(); +}); + +add_task(async function test_autofill_creditCards_autocomplete_off_pref() { + let document = `
    + + +
    `; + let expected = [null, null, null]; + info(`Set pref so that credit card autofill respects autocomplete="off"`); + Services.prefs.setBoolPref( + FormAutofill.AUTOFILL_CREDITCARDS_AUTOCOMPLETE_OFF_PREF, + false + ); + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + document + ); + let element = doc.getElementById("targetElement"); + let value = FormAutofillHeuristics.inferFieldInfo(element); + + Assert.deepEqual(value, expected); + document = `
    + + +
    `; + expected = ["cc-number", null, 1]; + info( + `Set pref so that credit card autofill does not respect autocomplete="off"` + ); + Services.prefs.setBoolPref( + FormAutofill.AUTOFILL_CREDITCARDS_AUTOCOMPLETE_OFF_PREF, + true + ); + doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + document + ); + element = doc.getElementById("targetElement"); + value = FormAutofillHeuristics.inferFieldInfo(element); + + Assert.deepEqual(value, expected); + Services.prefs.clearUserPref( + FormAutofill.AUTOFILL_CREDITCARDS_AUTOCOMPLETE_OFF_PREF + ); +}); + +add_task(async function test_autofill_addresses_autocomplete_off_pref() { + let document = `
    + +
    `; + let expected = [null, null, null]; + info(`Set pref so that address autofill respects autocomplete="off"`); + Services.prefs.setBoolPref( + FormAutofill.AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF, + false + ); + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + document + ); + let element = doc.getElementById("given-name"); + let value = FormAutofillHeuristics.inferFieldInfo(element); + + Assert.deepEqual(value, expected); + document = `
    + +
    `; + expected = ["given-name", null, null]; + info(`Set pref so that address autofill does not respect autocomplete="off"`); + Services.prefs.setBoolPref( + FormAutofill.AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF, + true + ); + doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + document + ); + element = doc.getElementById("given-name"); + value = FormAutofillHeuristics.inferFieldInfo(element); + + Assert.deepEqual(value, expected); + Services.prefs.clearUserPref( + FormAutofill.AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF + ); +}); diff --git a/browser/extensions/formautofill/test/unit/test_getRecords.js b/browser/extensions/formautofill/test/unit/test_getRecords.js new file mode 100644 index 0000000000..9a7e5e6ac7 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_getRecords.js @@ -0,0 +1,258 @@ +/* + * Test for make sure getRecords can retrieve right collection from storage. + */ + +"use strict"; + +const { CreditCard } = ChromeUtils.importESModule( + "resource://gre/modules/CreditCard.sys.mjs" +); + +let FormAutofillParent, FormAutofillStatus; +let OSKeyStore; +add_setup(async () => { + ({ FormAutofillParent, FormAutofillStatus } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillParent.sys.mjs" + )); + ({ OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" + )); +}); + +const TEST_ADDRESS_1 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+16172535702", + email: "timbl@w3.org", +}; + +const TEST_ADDRESS_2 = { + "street-address": "Some Address", + country: "US", +}; + +let TEST_CREDIT_CARD_1 = { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 4, + "cc-exp-year": 2017, + "cc-type": "visa", +}; + +let TEST_CREDIT_CARD_2 = { + "cc-name": "John Dai", + "cc-number": "4929001587121045", + "cc-exp-month": 2, + "cc-exp-year": 2017, + "cc-type": "visa", +}; + +add_task(async function test_getRecords() { + FormAutofillStatus.init(); + + await FormAutofillStatus.formAutofillStorage.initialize(); + let fakeResult = { + addresses: [ + { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + }, + ], + creditCards: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }, + ], + }; + + for (let collectionName of ["addresses", "creditCards", "nonExisting"]) { + let collection = FormAutofillStatus.formAutofillStorage[collectionName]; + let expectedResult = fakeResult[collectionName] || []; + + if (collection) { + sinon.stub(collection, "getAll"); + collection.getAll.returns(Promise.resolve(expectedResult)); + } + await FormAutofillParent._getRecords({ collectionName }); + if (collection) { + Assert.equal(collection.getAll.called, true); + collection.getAll.restore(); + } + } +}); + +add_task(async function test_getRecords_addresses() { + await FormAutofillStatus.formAutofillStorage.initialize(); + let mockAddresses = [TEST_ADDRESS_1, TEST_ADDRESS_2]; + let collection = FormAutofillStatus.formAutofillStorage.addresses; + sinon.stub(collection, "getAll"); + collection.getAll.returns(Promise.resolve(mockAddresses)); + + let testCases = [ + { + description: "If the search string could match 1 address", + filter: { + collectionName: "addresses", + info: { fieldName: "street-address" }, + searchString: "Some", + }, + expectedResult: [TEST_ADDRESS_2], + }, + { + description: "If the search string could match multiple addresses", + filter: { + collectionName: "addresses", + info: { fieldName: "country" }, + searchString: "u", + }, + expectedResult: [TEST_ADDRESS_1, TEST_ADDRESS_2], + }, + { + description: "If the search string could not match any address", + filter: { + collectionName: "addresses", + info: { fieldName: "street-address" }, + searchString: "test", + }, + expectedResult: [], + }, + { + description: "If the search string is empty", + filter: { + collectionName: "addresses", + info: { fieldName: "street-address" }, + searchString: "", + }, + expectedResult: [TEST_ADDRESS_1, TEST_ADDRESS_2], + }, + { + description: + "Check if the filtering logic is free from searching special chars", + filter: { + collectionName: "addresses", + info: { fieldName: "street-address" }, + searchString: ".*", + }, + expectedResult: [], + }, + { + description: + "Prevent broken while searching the property that does not exist", + filter: { + collectionName: "addresses", + info: { fieldName: "tel" }, + searchString: "1", + }, + expectedResult: [], + }, + ]; + + for (let testCase of testCases) { + info("Starting testcase: " + testCase.description); + let result = await FormAutofillParent._getRecords(testCase.filter); + Assert.deepEqual(result, testCase.expectedResult); + } +}); + +add_task(async function test_getRecords_creditCards() { + await FormAutofillStatus.formAutofillStorage.initialize(); + let collection = FormAutofillStatus.formAutofillStorage.creditCards; + let encryptedCCRecords = await Promise.all( + [TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2].map(async record => { + let clonedRecord = Object.assign({}, record); + clonedRecord["cc-number"] = CreditCard.getLongMaskedNumber( + record["cc-number"] + ); + clonedRecord["cc-number-encrypted"] = await OSKeyStore.encrypt( + record["cc-number"] + ); + return clonedRecord; + }) + ); + sinon + .stub(collection, "getAll") + .callsFake(() => + Promise.resolve([ + Object.assign({}, encryptedCCRecords[0]), + Object.assign({}, encryptedCCRecords[1]), + ]) + ); + + let testCases = [ + { + description: "If the search string could match multiple creditCards", + filter: { + collectionName: "creditCards", + info: { fieldName: "cc-name" }, + searchString: "John", + }, + expectedResult: encryptedCCRecords, + }, + { + description: "If the search string could not match any creditCard", + filter: { + collectionName: "creditCards", + info: { fieldName: "cc-name" }, + searchString: "T", + }, + expectedResult: [], + }, + { + description: + "Return all creditCards if focused field is cc number; " + + "if the search string could match multiple creditCards", + filter: { + collectionName: "creditCards", + info: { fieldName: "cc-number" }, + searchString: "4", + }, + expectedResult: encryptedCCRecords, + }, + { + description: "If the search string could match 1 creditCard", + filter: { + collectionName: "creditCards", + info: { fieldName: "cc-name" }, + searchString: "John Doe", + }, + mpEnabled: true, + expectedResult: encryptedCCRecords.slice(0, 1), + }, + { + description: "Return all creditCards if focused field is cc number", + filter: { + collectionName: "creditCards", + info: { fieldName: "cc-number" }, + searchString: "411", + }, + mpEnabled: true, + expectedResult: encryptedCCRecords, + }, + ]; + + for (let testCase of testCases) { + info("Starting testcase: " + testCase.description); + if (testCase.mpEnabled) { + let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance( + Ci.nsIPK11TokenDB + ); + let token = tokendb.getInternalKeyToken(); + token.reset(); + token.initPassword("password"); + } + let result = await FormAutofillParent._getRecords(testCase.filter); + Assert.deepEqual(result, testCase.expectedResult); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_isAddressAutofillAvailable.js b/browser/extensions/formautofill/test/unit/test_isAddressAutofillAvailable.js new file mode 100644 index 0000000000..2d2940c33e --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_isAddressAutofillAvailable.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test enabling address autofill in specific locales and regions. + */ + +"use strict"; + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +add_task(async function test_defaultTestEnvironment() { + Assert.equal( + Services.prefs.getCharPref("extensions.formautofill.addresses.supported"), + "on" + ); +}); + +add_task(async function test_default_supported_module_and_autofill_region() { + Services.prefs.setCharPref("browser.search.region", "US"); + registerCleanupFunction(function cleanupRegion() { + Services.prefs.clearUserPref("browser.search.region"); + }); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + await addon.reload(); + + Assert.equal(FormAutofill.isAutofillAddressesAvailable, true); + Assert.equal(FormAutofill.isAutofillAddressesEnabled, true); +}); + +add_task( + async function test_supported_creditCard_region_unsupported_address_region() { + Services.prefs.setCharPref( + "extensions.formautofill.addresses.supported", + "detect" + ); + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supported", + "detect" + ); + Services.prefs.setCharPref("browser.search.region", "FR"); + Services.prefs.setCharPref( + "extensions.formautofill.addresses.supportedCountries", + "US,CA" + ); + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supportedCountries", + "US,CA,FR" + ); + registerCleanupFunction(function cleanupPrefs() { + Services.prefs.clearUserPref("browser.search.region"); + Services.prefs.clearUserPref( + "extensions.formautofill.addresses.supportedCountries" + ); + Services.prefs.clearUserPref( + "extensions.formautofill.addresses.supported" + ); + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.supported" + ); + }); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + await addon.reload(); + Assert.ok( + Services.prefs.getBoolPref("extensions.formautofill.creditCards.enabled") + ); + Assert.equal(FormAutofill.isAutofillAddressesAvailable, false); + Assert.equal(FormAutofill.isAutofillAddressesEnabled, false); + } +); diff --git a/browser/extensions/formautofill/test/unit/test_isCJKName.js b/browser/extensions/formautofill/test/unit/test_isCJKName.js new file mode 100644 index 0000000000..f0e50b60f9 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_isCJKName.js @@ -0,0 +1,80 @@ +/** + * Tests the "isCJKName" function of FormAutofillNameUtils object. + */ + +"use strict"; + +var FormAutofillNameUtils; +add_setup(async () => { + ({ FormAutofillNameUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs" + )); +}); + +// Test cases is initially copied from +// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util_unittest.cc +const TESTCASES = [ + { + // Non-CJK language with only ASCII characters. + fullName: "Homer Jay Simpson", + expectedResult: false, + }, + { + // Non-CJK language with some ASCII characters. + fullName: "Éloïse Paré", + expectedResult: false, + }, + { + // Non-CJK language with no ASCII characters. + fullName: "Σωκράτης", + expectedResult: false, + }, + { + // (Simplified) Chinese name, Unihan. + fullName: "刘翔", + expectedResult: true, + }, + { + // (Simplified) Chinese name, Unihan, with an ASCII space. + fullName: "成 龙", + expectedResult: true, + }, + { + // Korean name, Hangul. + fullName: "송지효", + expectedResult: true, + }, + { + // Korean name, Hangul, with an 'IDEOGRAPHIC SPACE' (U+3000). + fullName: "김 종국", + expectedResult: true, + }, + { + // Japanese name, Unihan. + fullName: "山田貴洋", + expectedResult: true, + }, + { + // Japanese name, Katakana, with a 'KATAKANA MIDDLE DOT' (U+30FB). + fullName: "ビル・ゲイツ", + expectedResult: true, + }, + { + // Japanese name, Katakana, with a 'MIDDLE DOT' (U+00B7) (likely a typo). + fullName: "ビル·ゲイツ", + expectedResult: true, + }, + { + // CJK names don't have a middle name, so a 3-part name is bogus to us. + fullName: "반 기 문", + expectedResult: false, + }, +]; + +add_task(async function test_isCJKName() { + TESTCASES.forEach(testcase => { + info("Starting testcase: " + testcase.fullName); + let result = FormAutofillNameUtils._isCJKName(testcase.fullName); + Assert.equal(result, testcase.expectedResult); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_isCreditCardAutofillAvailable.js b/browser/extensions/formautofill/test/unit/test_isCreditCardAutofillAvailable.js new file mode 100644 index 0000000000..5be5101ee6 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_isCreditCardAutofillAvailable.js @@ -0,0 +1,84 @@ +/** + * Test enabling the feature in specific locales and regions. + */ + +"use strict"; + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +add_task(async function test_defaultTestEnvironment() { + Assert.ok(Services.prefs.getBoolPref("dom.forms.autocomplete.formautofill")); +}); + +add_task(async function test_detect_unsupportedRegion() { + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supported", + "detect" + ); + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supportedCountries", + "US,CA" + ); + Services.prefs.setCharPref("browser.search.region", "ZZ"); + registerCleanupFunction(function cleanupRegion() { + Services.prefs.clearUserPref("browser.search.region"); + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.supported" + ); + Services.prefs.clearUserPref("extensions.formautofill.addresses.supported"); + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.supportedCountries" + ); + }); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + await addon.reload(); + + Assert.equal( + FormAutofill.isAutofillCreditCardsAvailable, + false, + "Credit card autofill should not be available" + ); + Assert.equal( + FormAutofill.isAutofillCreditCardsEnabled, + false, + "Credit card autofill should not be enabled" + ); +}); + +add_task(async function test_detect_supportedRegion() { + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supported", + "detect" + ); + Services.prefs.setCharPref( + "extensions.formautofill.creditCards.supportedCountries", + "US,CA" + ); + Services.prefs.setCharPref("browser.search.region", "US"); + registerCleanupFunction(function cleanupRegion() { + Services.prefs.clearUserPref("browser.search.region"); + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.supported" + ); + Services.prefs.clearUserPref( + "extensions.formautofill.creditCards.supportedCountries" + ); + }); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + await addon.reload(); + + Assert.equal( + FormAutofill.isAutofillCreditCardsAvailable, + true, + "Credit card autofill should be available" + ); + Assert.equal( + FormAutofill.isAutofillCreditCardsEnabled, + true, + "Credit card autofill should be enabled" + ); +}); diff --git a/browser/extensions/formautofill/test/unit/test_isCreditCardOrAddressFieldType.js b/browser/extensions/formautofill/test/unit/test_isCreditCardOrAddressFieldType.js new file mode 100644 index 0000000000..872e9cfcda --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_isCreditCardOrAddressFieldType.js @@ -0,0 +1,103 @@ +"use strict"; + +var FormAutofillUtils; +add_setup(async () => { + ({ FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" + )); +}); + +const TESTCASES = [ + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: false, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: `
    `, + fieldId: "targetElement", + expectedResult: false, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, + { + document: ``, + fieldId: "targetElement", + expectedResult: true, + }, +]; + +TESTCASES.forEach(testcase => { + add_task(async function () { + info("Starting testcase: " + testcase.document); + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + + let field = doc.getElementById(testcase.fieldId); + Assert.equal( + FormAutofillUtils.isCreditCardOrAddressFieldType(field), + testcase.expectedResult + ); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_known_strings.js b/browser/extensions/formautofill/test/unit/test_known_strings.js new file mode 100644 index 0000000000..b3e69dc776 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_known_strings.js @@ -0,0 +1,148 @@ +"use strict"; +/* global FormAutofillHeuristics: true */ + +const KNOWN_NAMES = { + "cc-name": ["cc-name", "card-name", "cardholder-name", "cardholder"], + "cc-number": [ + "cc-number", + "cc-num", + "card-number", + "card-num", + "number", + "cc", + "cc-no", + "card-no", + "credit-card", + "numero-carte", + "carte", + "carte-credit", + "num-carte", + "cb-num", + ], + "cc-exp": [ + "cc-exp", + "card-exp", + "cc-expiration", + "card-expiration", + "cc-ex", + "card-ex", + "card-expire", + "card-expiry", + "validite", + "expiration", + "expiry", + "mm-yy", + "mm-yyyy", + "yy-mm", + "yyyy-mm", + "expiration-date", + "payment-card-expiration", + "payment-cc-date", + ], + "cc-exp-month": [ + "exp-month", + "cc-exp-month", + "cc-month", + "card-month", + "cc-mo", + "card-mo", + "exp-mo", + "card-exp-mo", + "cc-exp-mo", + "card-expiration-month", + "expiration-month", + "cc-mm", + "cc-m", + "card-mm", + "card-m", + "card-exp-mm", + "cc-exp-mm", + "exp-mm", + "exp-m", + "expire-month", + "expire-mo", + "expiry-month", + "expiry-mo", + "card-expire-month", + "card-expire-mo", + "card-expiry-month", + "card-expiry-mo", + "mois-validite", + "mois-expiration", + "m-validite", + "m-expiration", + "expiry-date-field-month", + "expiration-date-month", + "expiration-date-mm", + "exp-mon", + "validity-mo", + "exp-date-mo", + "cb-date-mois", + "date-m", + ], + "cc-exp-year": [ + "exp-year", + "cc-exp-year", + "cc-year", + "card-year", + "cc-yr", + "card-yr", + "exp-yr", + "card-exp-yr", + "cc-exp-yr", + "card-expiration-year", + "expiration-year", + "cc-yy", + "cc-y", + "card-yy", + "card-y", + "card-exp-yy", + "cc-exp-yy", + "exp-yy", + "exp-y", + "cc-yyyy", + "card-yyyy", + "card-exp-yyyy", + "cc-exp-yyyy", + "expire-year", + "expire-yr", + "expiry-year", + "expiry-yr", + "card-expire-year", + "card-expire-yr", + "card-expiry-year", + "card-expiry-yr", + "an-validite", + "an-expiration", + "annee-validite", + "annee-expiration", + "expiry-date-field-year", + "expiration-date-year", + "cb-date-ann", + "expiration-date-yy", + "expiration-date-yyyy", + "validity-year", + "exp-date-year", + "date-y", + ], +}; + +add_setup(async () => { + ({ FormAutofillHeuristics } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs" + )); +}); + +for (let field in KNOWN_NAMES) { + KNOWN_NAMES[field].forEach(name => { + add_task(async () => { + ok( + FormAutofillHeuristics.testRegex( + FormAutofillHeuristics.RULES[field], + name + ), + `RegExp for ${field} matches string '${name}'` + ); + }); + }); +} diff --git a/browser/extensions/formautofill/test/unit/test_markAsAutofillField.js b/browser/extensions/formautofill/test/unit/test_markAsAutofillField.js new file mode 100644 index 0000000000..72960fcaf2 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_markAsAutofillField.js @@ -0,0 +1,201 @@ +"use strict"; + +const TESTCASES = [ + { + description: "Form containing 8 fields with autocomplete attribute.", + document: `
    + + + + + + + + + + +
    `, + targetElementId: "given-name", + expectedResult: [ + "given-name", + "additional-name", + "family-name", + "street-addr", + "city", + "country", + "email", + "tel", + ], + }, + { + description: "Form containing only 2 fields with autocomplete attribute.", + document: `
    + + + + +
    `, + targetElementId: "street-addr", + expectedResult: [], + }, + { + description: "Fields without form element.", + document: ` + + + + + + `, + targetElementId: "street-addr", + expectedResult: ["street-addr", "city", "country", "email", "tel"], + }, + { + description: "Form containing credit card autocomplete attributes.", + document: `
    + + + + +
    `, + targetElementId: "cc-number", + expectedResult: ["cc-number", "cc-name", "cc-exp-month", "cc-exp-year"], + }, + { + description: + "Form containing multiple cc-number fields without autocomplete attributes.", + document: `
    + + + + + + + +
    `, + targetElementId: "cc-number1", + expectedResult: [ + "cc-number1", + "cc-number2", + "cc-number3", + "cc-number4", + "cc-name", + "cc-exp-month", + "cc-exp-year", + ], + }, + { + description: + "Invalid form containing three consecutive cc-number fields without autocomplete attributes.", + document: `
    + + + +
    `, + targetElementId: "cc-number1", + expectedResult: [], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "1.0", + ], + ], + }, + { + description: + "Invalid form containing five consecutive cc-number fields without autocomplete attributes.", + document: `
    + + + + + +
    `, + targetElementId: "cc-number1", + expectedResult: [], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "1.0", + ], + ], + }, + { + description: + "Valid form containing three consecutive cc-number fields without autocomplete attributes.", + document: `
    + + + + + + +
    `, + targetElementId: "cc-number1", + expectedResult: ["cc-number3", "cc-name", "cc-exp-month", "cc-exp-year"], + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "1.0", + ], + ], + }, + { + description: + "Valid form containing five consecutive cc-number fields without autocomplete attributes.", + document: `
    + + + + + + + + +
    `, + targetElementId: "cc-number1", + expectedResult: ["cc-number5", "cc-name", "cc-exp-month", "cc-exp-year"], + }, +]; + +let markedFieldId = []; + +var FormAutofillContent; +add_setup(async () => { + ({ FormAutofillContent } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillContent.sys.mjs" + )); + + FormAutofillContent._markAsAutofillField = function (field) { + markedFieldId.push(field.id); + }; +}); + +TESTCASES.forEach(testcase => { + add_task(async function () { + info("Starting testcase: " + testcase.description); + + if (testcase.prefs) { + testcase.prefs.forEach(pref => SetPref(pref[0], pref[1])); + } + + markedFieldId = []; + + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + let element = doc.getElementById(testcase.targetElementId); + FormAutofillContent.identifyAutofillFields(element); + + Assert.deepEqual( + markedFieldId, + testcase.expectedResult, + "Check the fields were marked correctly." + ); + + if (testcase.prefs) { + testcase.prefs.forEach(pref => Services.prefs.clearUserPref(pref[0])); + } + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_migrateRecords.js b/browser/extensions/formautofill/test/unit/test_migrateRecords.js new file mode 100644 index 0000000000..24b13b4322 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_migrateRecords.js @@ -0,0 +1,382 @@ +/** + * Tests the migration algorithm in profileStorage. + */ + +"use strict"; + +let FormAutofillStorage; +add_setup(async () => { + ({ FormAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + )); +}); + +const TEST_STORE_FILE_NAME = "test-profile.json"; + +const { ADDRESS_SCHEMA_VERSION } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorageBase.sys.mjs" +); +const { CREDIT_CARD_SCHEMA_VERSION } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorageBase.sys.mjs" +); + +const ADDRESS_TESTCASES = [ + { + description: + "The record version is equal to the current version. The migration shouldn't be invoked.", + record: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + name: "John", // The cached name field doesn't align "given-name" but it + // won't be recomputed because the migration isn't invoked. + }, + expectedResult: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + name: "John", + }, + }, + { + description: + "The record version is greater than the current version. The migration shouldn't be invoked.", + record: { + guid: "test-guid", + version: 99, + "given-name": "Timothy", + name: "John", + }, + expectedResult: { + guid: "test-guid", + version: 99, + "given-name": "Timothy", + name: "John", + }, + }, + { + description: + "The record version is less than the current version. The migration should be invoked.", + record: { + guid: "test-guid", + version: 0, + "given-name": "Timothy", + name: "John", + }, + expectedResult: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + name: "Timothy", + }, + }, + { + description: + "The record version is omitted. The migration should be invoked.", + record: { + guid: "test-guid", + "given-name": "Timothy", + name: "John", + "unknown-1": "an unknown field from another client", + }, + expectedResult: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + name: "Timothy", + "unknown-1": "an unknown field from another client", + }, + }, + { + description: + "The record version is an invalid value. The migration should be invoked.", + record: { + guid: "test-guid", + version: "ABCDE", + "given-name": "Timothy", + name: "John", + "unknown-1": "an unknown field from another client", + }, + expectedResult: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + name: "Timothy", + "unknown-1": "an unknown field from another client", + }, + }, + { + description: + "The omitted computed fields should be always recomputed even the record version is up-to-date.", + record: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + }, + expectedResult: { + guid: "test-guid", + version: ADDRESS_SCHEMA_VERSION, + "given-name": "Timothy", + name: "Timothy", + }, + }, + { + description: "The migration shouldn't be invoked on tombstones.", + record: { + guid: "test-guid", + timeLastModified: 12345, + deleted: true, + }, + expectedResult: { + guid: "test-guid", + timeLastModified: 12345, + deleted: true, + + // Make sure no new fields are appended. + version: undefined, + name: undefined, + }, + }, +]; + +const CREDIT_CARD_TESTCASES = [ + { + description: + "The record version is equal to the current version. The migration shouldn't be invoked.", + record: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + "cc-given-name": "John", // The cached "cc-given-name" field doesn't align + // "cc-name" but it won't be recomputed because + // the migration isn't invoked. + }, + expectedResult: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + "cc-given-name": "John", + }, + }, + { + description: + "The record version is greater than the current version. The migration shouldn't be invoked.", + record: { + guid: "test-guid", + version: 99, + "cc-name": "Timothy", + "cc-given-name": "John", + }, + expectedResult: { + guid: "test-guid", + version: 99, + "cc-name": "Timothy", + "cc-given-name": "John", + }, + }, + { + description: + "The record version is less than the current version. The migration should be invoked.", + record: { + guid: "test-guid", + version: 0, + "cc-name": "Timothy", + "cc-given-name": "John", + }, + expectedResult: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + "cc-given-name": "Timothy", + }, + }, + { + description: + "The record version is omitted. The migration should be invoked.", + record: { + guid: "test-guid", + "cc-name": "Timothy", + "cc-given-name": "John", + "unknown-1": "an unknown field from another client", + }, + expectedResult: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + "cc-given-name": "Timothy", + "unknown-1": "an unknown field from another client", + }, + }, + { + description: + "The record version is an invalid value. The migration should be invoked.", + record: { + guid: "test-guid", + version: "ABCDE", + "cc-name": "Timothy", + "cc-given-name": "John", + "unknown-1": "an unknown field from another client", + }, + expectedResult: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + "cc-given-name": "Timothy", + "unknown-1": "an unknown field from another client", + }, + }, + { + description: + "The omitted computed fields should be always recomputed even the record version is up-to-date.", + record: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + }, + expectedResult: { + guid: "test-guid", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Timothy", + "cc-given-name": "Timothy", + }, + }, + { + description: "The migration shouldn't be invoked on tombstones.", + record: { + guid: "test-guid", + timeLastModified: 12345, + deleted: true, + }, + expectedResult: { + guid: "test-guid", + timeLastModified: 12345, + deleted: true, + + // Make sure no new fields are appended. + version: undefined, + "cc-given-name": undefined, + }, + }, +]; + +let do_check_record_matches = (expectedRecord, record) => { + for (let key in expectedRecord) { + Assert.equal(expectedRecord[key], record[key]); + } +}; + +add_task(async function test_migrateAddressRecords() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + for (let testcase of ADDRESS_TESTCASES) { + info(testcase.description); + profileStorage._store.data.addresses = [testcase.record]; + await profileStorage.addresses._migrateRecord(testcase.record, 0); + do_check_record_matches( + testcase.expectedResult, + profileStorage.addresses._data[0] + ); + } +}); + +add_task(async function test_migrateCreditCardRecords() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + for (let testcase of CREDIT_CARD_TESTCASES) { + info(testcase.description); + profileStorage._store.data.creditCards = [testcase.record]; + await profileStorage.creditCards._migrateRecord(testcase.record, 0); + do_check_record_matches( + testcase.expectedResult, + profileStorage.creditCards._data[0] + ); + } +}); + +add_task(async function test_migrateEncryptedCreditCardNumber() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + info("v1 and v2 schema cards should be abandoned."); + + let v1record = { + guid: "test-guid1", + version: 1, + "cc-name": "Timothy", + "cc-number-encrypted": "aaaa", + }; + + let v2record = { + guid: "test-guid2", + version: 2, + "cc-name": "Bob", + "cc-number-encrypted": "bbbb", + }; + + profileStorage._store.data.creditCards = [v1record, v2record]; + await profileStorage.creditCards._migrateRecord(v1record, 0); + await profileStorage.creditCards._migrateRecord(v2record, 1); + v1record = profileStorage.creditCards._data[0]; + v2record = profileStorage.creditCards._data[1]; + + Assert.ok(v1record.deleted); + Assert.ok(v2record.deleted); +}); + +add_task(async function test_migrateDeprecatedCreditCardV4() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + let records = [ + { + guid: "test-guid1", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "Alice", + _sync: { + changeCounter: 0, + lastSyncedFields: {}, + }, + }, + { + guid: "test-guid2", + version: 4, + "cc-name": "Timothy", + _sync: { + changeCounter: 0, + lastSyncedFields: {}, + }, + }, + { + guid: "test-guid3", + version: 4, + "cc-name": "Bob", + }, + ]; + + profileStorage._store.data.creditCards = records; + for (let idx = 0; idx < records.length; idx++) { + await profileStorage.creditCards._migrateRecord(records[idx], idx); + } + + profileStorage.creditCards.pullSyncChanges(); + + // Record that has already synced before, do not sync again + equal(getSyncChangeCounter(profileStorage.creditCards, records[0].guid), 0); + + // alaways force sync v4 record + equal(records[1].version, CREDIT_CARD_SCHEMA_VERSION); + equal(getSyncChangeCounter(profileStorage.creditCards, records[1].guid), 1); + + equal(records[2].version, CREDIT_CARD_SCHEMA_VERSION); + equal(getSyncChangeCounter(profileStorage.creditCards, records[2].guid), 1); +}); diff --git a/browser/extensions/formautofill/test/unit/test_nameUtils.js b/browser/extensions/formautofill/test/unit/test_nameUtils.js new file mode 100644 index 0000000000..1b8bdb6d49 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_nameUtils.js @@ -0,0 +1,289 @@ +/** + * Tests FormAutofillNameUtils object. + */ + +"use strict"; + +var FormAutofillNameUtils; +add_task(async function () { + ({ FormAutofillNameUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs" + )); +}); + +// Test cases initially copied from +// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util_unittest.cc +const TESTCASES = [ + { + description: "Full name including given, middle and family names", + fullName: "Homer Jay Simpson", + nameParts: { + given: "Homer", + middle: "Jay", + family: "Simpson", + }, + }, + { + description: "No middle name", + fullName: "Moe Szyslak", + nameParts: { + given: "Moe", + middle: "", + family: "Szyslak", + }, + }, + { + description: "Common name prefixes removed", + fullName: "Reverend Timothy Lovejoy", + nameParts: { + given: "Timothy", + middle: "", + family: "Lovejoy", + }, + expectedFullName: "Timothy Lovejoy", + }, + { + description: "Common name suffixes removed", + fullName: "John Frink Phd", + nameParts: { + given: "John", + middle: "", + family: "Frink", + }, + expectedFullName: "John Frink", + }, + { + description: "Exception to the name suffix removal", + fullName: "John Ma", + nameParts: { + given: "John", + middle: "", + family: "Ma", + }, + }, + { + description: "Common family name prefixes not considered a middle name", + fullName: "Milhouse Van Houten", + nameParts: { + given: "Milhouse", + middle: "", + family: "Van Houten", + }, + }, + + // CJK names have reverse order (surname goes first, given name goes second). + { + description: "Chinese name, Unihan", + fullName: "孫 德明", + nameParts: { + given: "德明", + middle: "", + family: "孫", + }, + expectedFullName: "孫德明", + }, + { + description: 'Chinese name, Unihan, "IDEOGRAPHIC SPACE"', + fullName: "孫 德明", + nameParts: { + given: "德明", + middle: "", + family: "孫", + }, + expectedFullName: "孫德明", + }, + { + description: "Korean name, Hangul", + fullName: "홍 길동", + nameParts: { + given: "길동", + middle: "", + family: "홍", + }, + expectedFullName: "홍길동", + }, + { + description: "Japanese name, Unihan", + fullName: "山田 貴洋", + nameParts: { + given: "貴洋", + middle: "", + family: "山田", + }, + expectedFullName: "山田貴洋", + }, + + // In Japanese, foreign names use 'KATAKANA MIDDLE DOT' (U+30FB) as a + // separator. There is no consensus for the ordering. For now, we use the same + // ordering as regular Japanese names ("last・first"). + { + description: "Foreign name in Japanese, Katakana", + fullName: "ゲイツ・ビル", + nameParts: { + given: "ビル", + middle: "", + family: "ゲイツ", + }, + expectedFullName: "ゲイツビル", + }, + + // 'KATAKANA MIDDLE DOT' is occasionally typoed as 'MIDDLE DOT' (U+00B7). + { + description: "Foreign name in Japanese, Katakana", + fullName: "ゲイツ·ビル", + nameParts: { + given: "ビル", + middle: "", + family: "ゲイツ", + }, + expectedFullName: "ゲイツビル", + }, + + // CJK names don't usually have a space in the middle, but most of the time, + // the surname is only one character (in Chinese & Korean). + { + description: "Korean name, Hangul", + fullName: "최성훈", + nameParts: { + given: "성훈", + middle: "", + family: "최", + }, + }, + { + description: "(Simplified) Chinese name, Unihan", + fullName: "刘翔", + nameParts: { + given: "翔", + middle: "", + family: "刘", + }, + }, + { + description: "(Traditional) Chinese name, Unihan", + fullName: "劉翔", + nameParts: { + given: "翔", + middle: "", + family: "劉", + }, + }, + + // There are a few exceptions. Occasionally, the surname has two characters. + { + description: "Korean name, Hangul", + fullName: "남궁도", + nameParts: { + given: "도", + middle: "", + family: "남궁", + }, + }, + { + description: "Korean name, Hangul", + fullName: "황보혜정", + nameParts: { + given: "혜정", + middle: "", + family: "황보", + }, + }, + { + description: "(Traditional) Chinese name, Unihan", + fullName: "歐陽靖", + nameParts: { + given: "靖", + middle: "", + family: "歐陽", + }, + }, + + // In Korean, some 2-character surnames are rare/ambiguous, like "강전": "강" + // is a common surname, and "전" can be part of a given name. In those cases, + // we assume it's 1/2 for 3-character names, or 2/2 for 4-character names. + { + description: "Korean name, Hangul", + fullName: "강전희", + nameParts: { + given: "전희", + middle: "", + family: "강", + }, + }, + { + description: "Korean name, Hangul", + fullName: "황목치승", + nameParts: { + given: "치승", + middle: "", + family: "황목", + }, + }, + + // It occasionally happens that a full name is 2 characters, 1/1. + { + description: "Korean name, Hangul", + fullName: "이도", + nameParts: { + given: "도", + middle: "", + family: "이", + }, + }, + { + description: "Korean name, Hangul", + fullName: "孫文", + nameParts: { + given: "文", + middle: "", + family: "孫", + }, + }, + + // These are no CJK names for us, they're just bogus. + { + description: "Bogus", + fullName: "Homer シンプソン", + nameParts: { + given: "Homer", + middle: "", + family: "シンプソン", + }, + }, + { + description: "Bogus", + fullName: "ホーマー Simpson", + nameParts: { + given: "ホーマー", + middle: "", + family: "Simpson", + }, + }, + { + description: "CJK has a middle-name, too unusual", + fullName: "반 기 문", + nameParts: { + given: "반", + middle: "기", + family: "문", + }, + }, +]; + +add_task(async function test_splitName() { + TESTCASES.forEach(testcase => { + if (testcase.fullName) { + info("Starting testcase: " + testcase.description); + let nameParts = FormAutofillNameUtils.splitName(testcase.fullName); + Assert.deepEqual(nameParts, testcase.nameParts); + } + }); +}); + +add_task(async function test_joinName() { + TESTCASES.forEach(testcase => { + info("Starting testcase: " + testcase.description); + let name = FormAutofillNameUtils.joinNameParts(testcase.nameParts); + Assert.equal(name, testcase.expectedFullName || testcase.fullName); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js new file mode 100644 index 0000000000..1c252e04bb --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js @@ -0,0 +1,805 @@ +"use strict"; + +var FormAutofillContent; +add_setup(async () => { + ({ FormAutofillContent } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillContent.sys.mjs" + )); +}); + +const DEFAULT_TEST_DOC = `
    + + + + + + + + + + + + +
    `; +const TARGET_ELEMENT_ID = "street-addr"; + +const TESTCASES = [ + { + description: + "Should not trigger address saving if the number of fields is less than 3", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + tel: "1-650-903-0800", + }, + expectedResult: { + formSubmission: false, + }, + }, + { + description: "Should not trigger credit card saving if number is empty", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "cc-name": "John Doe", + "cc-exp-month": 12, + "cc-exp-year": 2000, + }, + expectedResult: { + formSubmission: false, + }, + }, + { + description: + "Should not trigger credit card saving if there is more than one cc-number field but less than four fields", + document: `
    + + + + + + + + +
    + `, + targetElementId: "cc-name", + formValue: { + "cc-name": "John Doe", + "cc-number1": "3714", + "cc-number2": "4963", + "cc-number3": "5398", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "amex", + }, + expectedResult: { + formSubmission: false, + }, + }, + { + description: "Trigger address saving", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + country: "US", + tel: "1-650-903-0800", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "", + "address-level2": "", + country: "US", + email: "", + tel: "1-650-903-0800", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Trigger credit card saving", + document: DEFAULT_TEST_DOC, + targetElementId: "cc-type", + formValue: { + "cc-name": "John Doe", + "cc-number": "5105105105105100", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "amex", + }, + expectedResult: { + formSubmission: true, + records: { + address: [], + creditCard: [ + { + guid: null, + record: { + "cc-name": "John Doe", + "cc-number": "5105105105105100", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "amex", + }, + untouchedFields: [], + }, + ], + }, + }, + }, + { + description: "Trigger credit card saving using multiple cc-number fields", + document: `
    + + + + + + + + + +
    `, + targetElementId: "cc-type", + formValue: { + "cc-name": "John Doe", + "cc-number1": "3714", + "cc-number2": "4963", + "cc-number3": "5398", + "cc-number4": "431", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "amex", + }, + expectedResult: { + formSubmission: true, + records: { + address: [], + creditCard: [ + { + guid: null, + record: { + "cc-name": "John Doe", + "cc-number": "371449635398431", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "amex", + }, + untouchedFields: [], + }, + ], + }, + }, + }, + { + description: "Trigger address and credit card saving", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + country: "US", + tel: "1-650-903-0800", + "cc-name": "John Doe", + "cc-number": "5105105105105100", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "visa", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "", + "address-level2": "", + country: "US", + email: "", + tel: "1-650-903-0800", + }, + untouchedFields: [], + }, + ], + creditCard: [ + { + guid: null, + record: { + "cc-name": "John Doe", + "cc-number": "5105105105105100", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "cc-type": "visa", + }, + untouchedFields: [], + }, + ], + }, + }, + }, + { + description: "Profile saved with trimmed string", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue ", + country: "US", + tel: " 1-650-903-0800", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "", + "address-level2": "", + country: "US", + email: "", + tel: "1-650-903-0800", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Eliminate the field that is empty after trimmed", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + country: "US", + email: " ", + tel: "1-650-903-0800", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "", + "address-level2": "", + country: "US", + email: "", + tel: "1-650-903-0800", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Save state with regular select option", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "CA", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "address-level1": "CA", + "address-level2": "", + "street-address": "331 E. Evelyn Avenue", + country: "US", + email: "", + tel: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Save state with lowercase value", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "ca", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "address-level1": "CA", + "address-level2": "", + "street-address": "331 E. Evelyn Avenue", + country: "US", + email: "", + tel: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Save state with a country code prefixed to the label", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "AR", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "address-level1": "AR", + "address-level2": "", + "street-address": "331 E. Evelyn Avenue", + country: "US", + email: "", + tel: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Save state with a country code prefixed to the value", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "US-CA", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "address-level1": "CA", + "address-level2": "", + "street-address": "331 E. Evelyn Avenue", + country: "US", + email: "", + tel: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: + "Save state with a country code prefixed to the value and label", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "US-AZ", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "address-level1": "AZ", + "address-level2": "", + "street-address": "331 E. Evelyn Avenue", + country: "US", + email: "", + tel: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: + "Should save select label instead when failed to abbreviate the value", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "Ariz", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "address-level1": "Arizonac", + "address-level2": "", + "street-address": "331 E. Evelyn Avenue", + country: "US", + email: "", + tel: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Shouldn't save select with multiple selections", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": ["AL", "AK", "AP"], + "street-addr": "331 E. Evelyn Avenue", + country: "US", + tel: "1-650-903-0800", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "", + "address-level2": "", + country: "US", + tel: "1-650-903-0800", + email: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Shouldn't save select with empty value", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "address-level1": "", + "street-addr": "331 E. Evelyn Avenue", + country: "US", + tel: "1-650-903-0800", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "", + "address-level2": "", + country: "US", + tel: "1-650-903-0800", + email: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Shouldn't save tel whose length is too short", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + "address-level1": "CA", + country: "US", + tel: "1234", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "CA", + "address-level2": "", + country: "US", + tel: "", + email: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Shouldn't save tel whose length is too long", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + "address-level1": "CA", + country: "US", + tel: "1234567890123456", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "CA", + "address-level2": "", + country: "US", + tel: "", + email: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, + { + description: "Shouldn't save tel which contains invalid characters", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "street-addr": "331 E. Evelyn Avenue", + "address-level1": "CA", + country: "US", + tel: "12345###!!", + }, + expectedResult: { + formSubmission: true, + records: { + address: [ + { + guid: null, + record: { + "street-address": "331 E. Evelyn Avenue", + "address-level1": "CA", + "address-level2": "", + country: "US", + tel: "", + email: "", + }, + untouchedFields: [], + }, + ], + creditCard: [], + }, + }, + }, +]; + +add_task(async function handle_invalid_form() { + info("Starting testcase: Test an invalid form element"); + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test", + DEFAULT_TEST_DOC + ); + let fakeForm = doc.createElement("form"); + sinon.spy(FormAutofillContent, "_onFormSubmit"); + + FormAutofillContent.formSubmitted(fakeForm, null); + Assert.equal(FormAutofillContent._onFormSubmit.called, false); + FormAutofillContent._onFormSubmit.restore(); +}); + +add_task(async function autofill_disabled() { + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test", + DEFAULT_TEST_DOC + ); + let form = doc.getElementById("form1"); + form.reset(); + + let testcase = { + "street-addr": "331 E. Evelyn Avenue", + country: "US", + tel: "+16509030800", + "cc-number": "1111222233334444", + }; + for (let key in testcase) { + let input = doc.getElementById(key); + input.value = testcase[key]; + } + + let element = doc.getElementById(TARGET_ELEMENT_ID); + FormAutofillContent.identifyAutofillFields(element); + + sinon.stub(FormAutofillContent, "_onFormSubmit"); + + // "_onFormSubmit" shouldn't be called if both "addresses" and "creditCards" + // are disabled. + Services.prefs.setBoolPref( + "extensions.formautofill.addresses.enabled", + false + ); + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + false + ); + FormAutofillContent.formSubmitted(form, null); + Assert.equal(FormAutofillContent._onFormSubmit.called, false); + FormAutofillContent._onFormSubmit.resetHistory(); + + // "_onFormSubmit" should be called as usual. + Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); + Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); + + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + true + ); + + FormAutofillContent.formSubmitted(form, null); + Assert.equal(FormAutofillContent._onFormSubmit.called, true); + Assert.notDeepEqual(FormAutofillContent._onFormSubmit.args[0][0].address, []); + Assert.notDeepEqual( + FormAutofillContent._onFormSubmit.args[0][0].creditCard, + [] + ); + FormAutofillContent._onFormSubmit.resetHistory(); + + // "address" should be empty if "addresses" pref is disabled. + Services.prefs.setBoolPref( + "extensions.formautofill.addresses.enabled", + false + ); + FormAutofillContent.formSubmitted(form, null); + Assert.equal(FormAutofillContent._onFormSubmit.called, true); + Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0].address, []); + Assert.notDeepEqual( + FormAutofillContent._onFormSubmit.args[0][0].creditCard, + [] + ); + FormAutofillContent._onFormSubmit.resetHistory(); + Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); + + // "creditCard" should be empty if "creditCards" pref is disabled. + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + false + ); + FormAutofillContent.formSubmitted(form, null); + Assert.deepEqual(FormAutofillContent._onFormSubmit.called, true); + Assert.notDeepEqual(FormAutofillContent._onFormSubmit.args[0][0].address, []); + Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0].creditCard, []); + FormAutofillContent._onFormSubmit.resetHistory(); + Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); + + FormAutofillContent._onFormSubmit.restore(); +}); + +TESTCASES.forEach(testcase => { + add_task(async function check_records_saving_is_called_correctly() { + info("Starting testcase: " + testcase.description); + + Services.prefs.setBoolPref( + "extensions.formautofill.creditCards.enabled", + true + ); + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + let form = doc.getElementById("form1"); + form.reset(); + for (let key in testcase.formValue) { + let input = doc.getElementById(key); + let value = testcase.formValue[key]; + if (ChromeUtils.getClassName(input) === "HTMLSelectElement" && value) { + input.multiple = Array.isArray(value); + [...input.options].forEach(option => { + option.selected = value.includes(option.value); + }); + } else { + input.value = testcase.formValue[key]; + } + } + sinon.stub(FormAutofillContent, "_onFormSubmit"); + + let element = doc.getElementById(testcase.targetElementId); + FormAutofillContent.identifyAutofillFields(element); + FormAutofillContent.formSubmitted(form, null); + + Assert.equal( + FormAutofillContent._onFormSubmit.called, + testcase.expectedResult.formSubmission, + "Check expected onFormSubmit.called" + ); + if (FormAutofillContent._onFormSubmit.called) { + for (let ccRecord of FormAutofillContent._onFormSubmit.args[0][0] + .creditCard) { + delete ccRecord.flowId; + } + for (let addrRecord of FormAutofillContent._onFormSubmit.args[0][0] + .address) { + delete addrRecord.flowId; + } + + Assert.deepEqual( + FormAutofillContent._onFormSubmit.args[0][0], + testcase.expectedResult.records + ); + } + FormAutofillContent._onFormSubmit.restore(); + Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_parseAddressFormat.js b/browser/extensions/formautofill/test/unit/test_parseAddressFormat.js new file mode 100644 index 0000000000..df6c3e3069 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_parseAddressFormat.js @@ -0,0 +1,66 @@ +"use strict"; + +var FormAutofillUtils; +add_setup(async () => { + ({ FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" + )); +}); + +add_task(async function test_parseAddressFormat() { + const TEST_CASES = [ + { + fmt: "%N%n%O%n%A%n%C, %S %Z", // US + parsed: [ + { fieldId: "name", newLine: true }, + { fieldId: "organization", newLine: true }, + { fieldId: "street-address", newLine: true }, + { fieldId: "address-level2" }, + { fieldId: "address-level1" }, + { fieldId: "postal-code" }, + ], + }, + { + fmt: "%N%n%O%n%A%n%C %S %Z", // CA + parsed: [ + { fieldId: "name", newLine: true }, + { fieldId: "organization", newLine: true }, + { fieldId: "street-address", newLine: true }, + { fieldId: "address-level2" }, + { fieldId: "address-level1" }, + { fieldId: "postal-code" }, + ], + }, + { + fmt: "%N%n%O%n%A%n%Z %C", // DE + parsed: [ + { fieldId: "name", newLine: true }, + { fieldId: "organization", newLine: true }, + { fieldId: "street-address", newLine: true }, + { fieldId: "postal-code" }, + { fieldId: "address-level2" }, + ], + }, + { + fmt: "%N%n%O%n%A%n%D%n%C%n%S %Z", // IE + parsed: [ + { fieldId: "name", newLine: true }, + { fieldId: "organization", newLine: true }, + { fieldId: "street-address", newLine: true }, + { fieldId: "address-level3", newLine: true }, + { fieldId: "address-level2", newLine: true }, + { fieldId: "address-level1" }, + { fieldId: "postal-code" }, + ], + }, + ]; + + Assert.throws( + () => FormAutofillUtils.parseAddressFormat(), + /fmt string is missing./, + "Should throw if fmt is empty" + ); + for (let tc of TEST_CASES) { + Assert.deepEqual(FormAutofillUtils.parseAddressFormat(tc.fmt), tc.parsed); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_parseStreetAddress.js b/browser/extensions/formautofill/test/unit/test_parseStreetAddress.js new file mode 100644 index 0000000000..dc924d2ce8 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_parseStreetAddress.js @@ -0,0 +1,74 @@ +"use strict"; + +const { AddressParser } = ChromeUtils.import( + "resource://gre/modules/shared/AddressParser.jsm" +); + +// To add a new test entry to a "TESTCASES" variable, +// you would need to create a new array containing two elements. +// - The first element is a string representing a street address to be parsed. +// - The second element is an array containing the expected output after parsing the address, +// which should follow the format of +// [street number, street name, apartment number (if applicable), floor number (if applicable)]. +// +// Note. If we expect the passed street address cannot be parsed, set the second element to null. +const TESTCASES = [ + ["123 Main St. Apt 4, Floor 2", [123, "Main St.", "4", 2]], + ["32 Vassar Street MIT Room 4", [32, "Vassar Street MIT", "4", null]], + ["32 Vassar Street MIT", [32, "Vassar Street MIT", null, null]], + [ + "32 Vassar Street MIT Room 32-G524", + [32, "Vassar Street MIT", "32-G524", null], + ], + ["163 W Hastings\nSuite 209", [163, "W Hastings", "209", null]], + ["1234 Elm St. Apt 4, Floor 2", [1234, "Elm St.", "4", 2]], + ["456 Oak Drive, Unit 2A", [456, "Oak Drive", "2A", null]], + ["789 Maple Ave, Suite 300", [789, "Maple Ave", "300", null]], + ["321 Willow Lane, #5", [321, "Willow Lane", "5", null]], + ["654 Pine Circle, Apt B", [654, "Pine Circle", "B", null]], + ["987 Birch Court, 3rd Floor", [987, "Birch Court", null, 3]], + ["234 Cedar Way, Unit 6-2", [234, "Cedar Way", "6-2", null]], + ["345 Cherry St, Ste 12", [345, "Cherry St", "12", null]], + ["234 Palm St, Bldg 1, Apt 12", null], +]; + +add_task(async function test_parseStreetAddress() { + for (const TEST of TESTCASES) { + let [address, expected] = TEST; + const result = AddressParser.parseStreetAddress(address); + if (!expected) { + Assert.equal(result, null, "Expect failure to parse this street address"); + continue; + } + + const options = { + trim: true, + ignore_case: true, + }; + + const expectedSN = AddressParser.normalizeString(expected[0], options); + Assert.equal( + result.street_number, + expectedSN, + `expect street number to be ${expectedSN}, but got ${result.street_number}` + ); + const expectedSNA = AddressParser.normalizeString(expected[1], options); + Assert.equal( + result.street_name, + expectedSNA, + `expect street name to be ${expectedSNA}, but got ${result.street_name}` + ); + const expectedAN = AddressParser.normalizeString(expected[2], options); + Assert.equal( + result.apartment_number, + expectedAN, + `expect apartment number to be ${expectedAN}, but got ${result.apartment_number}` + ); + const expectedFN = AddressParser.normalizeString(expected[3], options); + Assert.equal( + result.floor_number, + expectedFN, + `expect floor number to be ${expectedFN}, but got ${result.floor_number}` + ); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_phoneNumber.js b/browser/extensions/formautofill/test/unit/test_phoneNumber.js new file mode 100644 index 0000000000..1c1d67e166 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_phoneNumber.js @@ -0,0 +1,399 @@ +/** + * Tests PhoneNumber.jsm and PhoneNumberNormalizer.jsm. + */ + +"use strict"; + +var PhoneNumber, PhoneNumberNormalizer; +add_setup(async () => { + ({ PhoneNumber } = ChromeUtils.importESModule( + "resource://autofill/phonenumberutils/PhoneNumber.sys.mjs" + )); + ({ PhoneNumberNormalizer } = ChromeUtils.importESModule( + "resource://autofill/phonenumberutils/PhoneNumberNormalizer.sys.mjs" + )); +}); + +function IsPlain(dial, expected) { + let result = PhoneNumber.IsPlain(dial); + Assert.equal(result, expected); +} + +function Normalize(dial, expected) { + let result = PhoneNumberNormalizer.Normalize(dial); + Assert.equal(result, expected); +} + +function CantParse(dial, currentRegion) { + let result = PhoneNumber.Parse(dial, currentRegion); + Assert.equal(null, result); +} + +function Parse(dial, currentRegion) { + let result = PhoneNumber.Parse(dial, currentRegion); + Assert.notEqual(result, null); + return result; +} + +function Test(dial, currentRegion, nationalNumber, region) { + let result = Parse(dial, currentRegion); + Assert.equal(result.nationalNumber, nationalNumber); + Assert.equal(result.region, region); + return result; +} + +function TestProperties(dial, currentRegion) { + let result = Parse(dial, currentRegion); + Assert.ok(result.internationalFormat); + Assert.ok(result.internationalNumber); + Assert.ok(result.nationalFormat); + Assert.ok(result.nationalNumber); + Assert.ok(result.countryName); + Assert.ok(result.countryCode); +} + +function Format( + dial, + currentRegion, + nationalNumber, + region, + nationalFormat, + internationalFormat +) { + let result = Test(dial, currentRegion, nationalNumber, region); + Assert.equal(result.nationalFormat, nationalFormat); + Assert.equal(result.internationalFormat, internationalFormat); + return result; +} + +function AllEqual(list, currentRegion) { + let parsedList = list.map(item => Parse(item, currentRegion)); + let firstItem = parsedList.shift(); + for (let item of parsedList) { + Assert.deepEqual(item, firstItem); + } +} + +add_task(async function test_phoneNumber() { + // Test whether could a string be a phone number. + IsPlain(null, false); + IsPlain("", false); + IsPlain("1", true); + IsPlain("*2", true); // Real number used in Venezuela + IsPlain("*8", true); // Real number used in Venezuela + IsPlain("12", true); + IsPlain("123", true); + IsPlain("1a2", false); + IsPlain("12a", false); + IsPlain("1234", true); + IsPlain("123a", false); + IsPlain("+", true); + IsPlain("+1", true); + IsPlain("+12", true); + IsPlain("+123", true); + IsPlain("()123", false); + IsPlain("(1)23", false); + IsPlain("(12)3", false); + IsPlain("(123)", false); + IsPlain("(123)4", false); + IsPlain("(123)4", false); + IsPlain("123;ext=", false); + IsPlain("123;ext=1", false); + IsPlain("123;ext=1234567", false); + IsPlain("123;ext=12345678", false); + IsPlain("123 ext:1", false); + IsPlain("123 ext:1#", false); + IsPlain("123-1#", false); + IsPlain("123 1#", false); + IsPlain("123 12345#", false); + IsPlain("123 +123456#", false); + + // Getting international number back from intl number. + TestProperties("+19497262896"); + + // Test parsing national numbers. + Parse("033316005", "NZ"); + Parse("03-331 6005", "NZ"); + Parse("03 331 6005", "NZ"); + // Testing international prefixes. + // Should strip country code. + Parse("0064 3 331 6005", "NZ"); + + // Test CA before US because CA has to import meta-information for US. + Parse("4031234567", "CA"); + Parse("(416) 585-4319", "CA"); + Parse("647-967-4357", "CA"); + Parse("416-716-8768", "CA"); + Parse("18002684646", "CA"); + Parse("416-445-9119", "CA"); + Parse("1-800-668-6866", "CA"); + Parse("(416) 453-6486", "CA"); + Parse("(647) 268-4778", "CA"); + Parse("647-218-1313", "CA"); + Parse("+1 647-209-4642", "CA"); + Parse("416-559-0133", "CA"); + Parse("+1 647-639-4118", "CA"); + Parse("+12898803664", "CA"); + Parse("780-901-4687", "CA"); + Parse("+14167070550", "CA"); + Parse("+1-647-522-6487", "CA"); + Parse("(416) 877-0880", "CA"); + + // Try again, but this time we have an international number with region rode US. It should + // recognize the country code and parse accordingly. + Parse("01164 3 331 6005", "US"); + Parse("+64 3 331 6005", "US"); + Parse("64(0)64123456", "NZ"); + // Check that using a "/" is fine in a phone number. + Parse("123/45678", "DE"); + Parse("123-456-7890", "US"); + + // Test parsing international numbers. + Parse("+1 (650) 333-6000", "NZ"); + Parse("1-650-333-6000", "US"); + // Calling the US number from Singapore by using different service providers + // 1st test: calling using SingTel IDD service (IDD is 001) + Parse("0011-650-333-6000", "SG"); + // 2nd test: calling using StarHub IDD service (IDD is 008) + Parse("0081-650-333-6000", "SG"); + // 3rd test: calling using SingTel V019 service (IDD is 019) + Parse("0191-650-333-6000", "SG"); + // Calling the US number from Poland + Parse("0~01-650-333-6000", "PL"); + // Using "++" at the start. + Parse("++1 (650) 333-6000", "PL"); + // Using a full-width plus sign. + Parse("\uFF0B1 (650) 333-6000", "SG"); + // The whole number, including punctuation, is here represented in full-width form. + Parse( + "\uFF0B\uFF11\u3000\uFF08\uFF16\uFF15\uFF10\uFF09" + + "\u3000\uFF13\uFF13\uFF13\uFF0D\uFF16\uFF10\uFF10\uFF10", + "SG" + ); + + // Test parsing with leading zeros. + Parse("+39 02-36618 300", "NZ"); + Parse("02-36618 300", "IT"); + Parse("312 345 678", "IT"); + + // Test parsing numbers in Argentina. + Parse("+54 9 343 555 1212", "AR"); + Parse("0343 15 555 1212", "AR"); + Parse("+54 9 3715 65 4320", "AR"); + Parse("03715 15 65 4320", "AR"); + Parse("+54 11 3797 0000", "AR"); + Parse("011 3797 0000", "AR"); + Parse("+54 3715 65 4321", "AR"); + Parse("03715 65 4321", "AR"); + Parse("+54 23 1234 0000", "AR"); + Parse("023 1234 0000", "AR"); + + // Test numbers in Mexico + Parse("+52 (449)978-0001", "MX"); + Parse("01 (449)978-0001", "MX"); + Parse("(449)978-0001", "MX"); + Parse("+52 1 33 1234-5678", "MX"); + Parse("044 (33) 1234-5678", "MX"); + Parse("045 33 1234-5678", "MX"); + + // Test that lots of spaces are ok. + Parse("0 3 3 3 1 6 0 0 5", "NZ"); + + // Test omitting the current region. This is only valid when the number starts + // with a '+'. + Parse("+64 3 331 6005"); + Parse("+64 3 331 6005", null); + + // US numbers + Format( + "19497261234", + "US", + "9497261234", + "US", + "(949) 726-1234", + "+1 949-726-1234" + ); + + // Try a couple german numbers from the US with various access codes. + Format( + "49451491934", + "US", + "0451491934", + "DE", + "0451 491934", + "+49 451 491934" + ); + Format( + "+49451491934", + "US", + "0451491934", + "DE", + "0451 491934", + "+49 451 491934" + ); + Format( + "01149451491934", + "US", + "0451491934", + "DE", + "0451 491934", + "+49 451 491934" + ); + + // Now try dialing the same number from within the German region. + Format( + "451491934", + "DE", + "0451491934", + "DE", + "0451 491934", + "+49 451 491934" + ); + Format( + "0451491934", + "DE", + "0451491934", + "DE", + "0451 491934", + "+49 451 491934" + ); + + // Numbers in italy keep the leading 0 in the city code when dialing internationally. + Format( + "0577-555-555", + "IT", + "0577555555", + "IT", + "05 7755 5555", + "+39 05 7755 5555" + ); + + // Colombian international number without the leading "+" + Format("5712234567", "CO", "12234567", "CO", "(1) 2234567", "+57 1 2234567"); + + // Telefonica tests + Format( + "612123123", + "ES", + "612123123", + "ES", + "612 12 31 23", + "+34 612 12 31 23" + ); + + // Chile mobile number from a landline + Format( + "0997654321", + "CL", + "997654321", + "CL", + "(99) 765 4321", + "+56 99 765 4321" + ); + + // Chile mobile number from another mobile number + Format( + "997654321", + "CL", + "997654321", + "CL", + "(99) 765 4321", + "+56 99 765 4321" + ); + + // Dialing 911 in the US. This is not a national number. + CantParse("911", "US"); + + // China mobile number with a 0 in it + Format( + "15955042864", + "CN", + "015955042864", + "CN", + "0159 5504 2864", + "+86 159 5504 2864" + ); + + // Testing international region numbers. + CantParse("883510000000091", "001"); + Format( + "+883510000000092", + "001", + "510000000092", + "001", + "510 000 000 092", + "+883 510 000 000 092" + ); + Format( + "883510000000093", + "FR", + "510000000093", + "001", + "510 000 000 093", + "+883 510 000 000 093" + ); + Format( + "+883510000000094", + "FR", + "510000000094", + "001", + "510 000 000 094", + "+883 510 000 000 094" + ); + Format( + "883510000000095", + "US", + "510000000095", + "001", + "510 000 000 095", + "+883 510 000 000 095" + ); + Format( + "+883510000000096", + "US", + "510000000096", + "001", + "510 000 000 096", + "+883 510 000 000 096" + ); + CantParse("979510000012", "001"); + Format( + "+979510000012", + "001", + "510000012", + "001", + "5 1000 0012", + "+979 5 1000 0012" + ); + + // Test normalizing numbers. Only 0-9,#* are valid in a phone number. + Normalize("+ABC # * , 9 _ 1 _0", "+222#*,910"); + Normalize("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "22233344455566677778889999"); + Normalize("abcdefghijklmnopqrstuvwxyz", "22233344455566677778889999"); + + // 8 and 9 digit numbers with area code in Brazil with collect call prefix (90) + AllEqual( + [ + "01187654321", + "0411187654321", + "551187654321", + "90411187654321", + "+551187654321", + ], + "BR" + ); + AllEqual( + [ + "011987654321", + "04111987654321", + "5511987654321", + "904111987654321", + "+5511987654321", + ], + "BR" + ); + + Assert.equal(PhoneNumberNormalizer.Normalize("123abc", true), "123"); + Assert.equal(PhoneNumberNormalizer.Normalize("12345", true), "12345"); + Assert.equal(PhoneNumberNormalizer.Normalize("1abcd", false), "12223"); +}); diff --git a/browser/extensions/formautofill/test/unit/test_previewFormFields.js b/browser/extensions/formautofill/test/unit/test_previewFormFields.js new file mode 100644 index 0000000000..a75f24de20 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_previewFormFields.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Bug 1762063 - we need to fix this pattern of having to wrap destructuring calls in parentheses. +// We can't do a standard destructuring call because FormAutofillUtils is already declared as a var in head.js +({ FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" +)); +const { FIELD_STATES } = FormAutofillUtils; +const PREVIEW = FIELD_STATES.PREVIEW; +const NORMAL = FIELD_STATES.NORMAL; + +const { OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" +); + +const TESTCASES = [ + { + description: "Preview best case address form", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + "given-name": "John", + "family-name": "Doe", + "street-address": "100 Main Street", + "address-level2": "Hamilton", + }, + expectedResultState: { + "given-name": PREVIEW, + "family-name": PREVIEW, + "street-address": PREVIEW, + "address-level2": PREVIEW, + }, + }, + { + description: "Preview form with a readonly input and non-readonly inputs", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + "given-name": "John", + "family-name": "Doe", + "street-address": "100 Main Street", + city: "Hamilton", + }, + expectedResultState: { + "given-name": PREVIEW, + "family-name": PREVIEW, + "street-address": PREVIEW, + "address-level2": undefined, + }, + }, + { + description: "Preview form with a disabled input and non-disabled inputs", + document: `
    + + + + +
    `, + focusedInputId: "given-name", + profileData: { + "given-name": "John", + "family-name": "Doe", + "street-address": "100 Main Street", + country: "CA", + }, + expectedResultState: { + "given-name": PREVIEW, + "family-name": PREVIEW, + "street-address": PREVIEW, + country: undefined, + }, + }, + { + description: + "Preview form with autocomplete select elements and matching option values", + document: `
    + + + +
    `, + focusedInputId: "given-name", + profileData: { + country: "US", + "address-level1": "CA", + }, + expectedResultState: { + "given-name": NORMAL, + country: PREVIEW, + "address-level1": PREVIEW, + }, + }, + { + description: "Preview best case credit card form", + document: `
    + + + + + +
    + `, + focusedInputId: "cc-number", + profileData: { + guid: "123", + "cc-number": "4111111111111111", + "cc-name": "test name", + "cc-exp-month": 6, + "cc-exp-year": 25, + }, + expectedResultState: { + "cc-number": PREVIEW, + "cc-name": PREVIEW, + "cc-exp-month": PREVIEW, + "cc-exp-year": PREVIEW, + "cc-csc": NORMAL, + }, + }, +]; + +function run_tests(testcases) { + for (let testcase of testcases) { + add_task(async function () { + info("Starting testcase: " + testcase.description); + let doc = MockDocument.createTestDocument( + "http://localhost:8080/test/", + testcase.document + ); + let form = doc.querySelector("form"); + let formLike = FormLikeFactory.createFromForm(form); + let handler = new FormAutofillHandler(formLike); + + // Replace the internal decrypt method with OSKeyStore API, + // but don't pass the reauth parameter to avoid triggering + // reauth login dialog in these tests. + let decryptHelper = async (cipherText, reauth) => { + return OSKeyStore.decrypt(cipherText, false); + }; + handler.collectFormFields(); + + let focusedInput = doc.getElementById(testcase.focusedInputId); + try { + handler.focusedInput = focusedInput; + } catch (e) { + if (e.message.includes("WeakMap key must be an object")) { + throw new Error( + `Couldn't find the focusedInputId in the current form! Make sure focusedInputId exists in your test form! testcase description:${testcase.description}` + ); + } else { + throw e; + } + } + + for (let section of handler.sections) { + section._decrypt = decryptHelper; + } + + let [adaptedProfile] = handler.activeSection.getAdaptedProfiles([ + testcase.profileData, + ]); + + await handler.activeSection.previewFormFields(adaptedProfile); + + for (let field of handler.fieldDetails) { + let actual = handler.getFilledStateByElement( + field.elementWeakRef.get() + ); + let expected = testcase.expectedResultState[field.fieldName]; + info(`Checking ${field.fieldName} state`); + Assert.equal( + actual, + expected, + "Check if preview state is set correctly" + ); + } + }); + } +} + +run_tests(TESTCASES); diff --git a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js new file mode 100644 index 0000000000..7a28f64634 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js @@ -0,0 +1,450 @@ +"use strict"; + +var AddressResult, CreditCardResult; +add_setup(async () => { + ({ AddressResult, CreditCardResult } = ChromeUtils.importESModule( + "resource://autofill/ProfileAutoCompleteResult.sys.mjs" + )); +}); + +let matchingProfiles = [ + { + guid: "test-guid-1", + "given-name": "Timothy", + "family-name": "Berners-Lee", + name: "Timothy Berners-Lee", + organization: "Sesame Street", + "street-address": "123 Sesame Street.", + "address-line1": "123 Sesame Street.", + tel: "1-345-345-3456.", + }, + { + guid: "test-guid-2", + "given-name": "John", + "family-name": "Doe", + name: "John Doe", + organization: "Mozilla", + "street-address": "331 E. Evelyn Avenue", + "address-line1": "331 E. Evelyn Avenue", + tel: "1-650-903-0800", + }, + { + guid: "test-guid-3", + organization: "", + "street-address": "321, No Name St. 2nd line 3rd line", + "-moz-street-address-one-line": "321, No Name St. 2nd line 3rd line", + "address-line1": "321, No Name St.", + "address-line2": "2nd line", + "address-line3": "3rd line", + tel: "1-000-000-0000", + }, +]; + +let allFieldNames = [ + "given-name", + "family-name", + "street-address", + "address-line1", + "address-line2", + "address-line3", + "organization", + "tel", +]; + +let addressTestCases = [ + { + description: "Focus on an `organization` field", + options: {}, + matchingProfiles, + allFieldNames, + searchString: "", + fieldName: "organization", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + defaultIndex: 0, + items: [ + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[0]), + label: JSON.stringify({ + primary: "Sesame Street", + secondary: "123 Sesame Street.", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[1]), + label: JSON.stringify({ + primary: "Mozilla", + secondary: "331 E. Evelyn Avenue", + }), + image: "", + }, + ], + }, + }, + { + description: "Focus on an `tel` field", + options: {}, + matchingProfiles, + allFieldNames, + searchString: "", + fieldName: "tel", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + defaultIndex: 0, + items: [ + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[0]), + label: JSON.stringify({ + primary: "1-345-345-3456.", + secondary: "123 Sesame Street.", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[1]), + label: JSON.stringify({ + primary: "1-650-903-0800", + secondary: "331 E. Evelyn Avenue", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[2]), + label: JSON.stringify({ + primary: "1-000-000-0000", + secondary: "321, No Name St. 2nd line 3rd line", + }), + image: "", + }, + ], + }, + }, + { + description: "Focus on an `street-address` field", + options: {}, + matchingProfiles, + allFieldNames, + searchString: "", + fieldName: "street-address", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + defaultIndex: 0, + items: [ + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[0]), + label: JSON.stringify({ + primary: "123 Sesame Street.", + secondary: "Timothy Berners-Lee", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[1]), + label: JSON.stringify({ + primary: "331 E. Evelyn Avenue", + secondary: "John Doe", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[2]), + label: JSON.stringify({ + primary: "321, No Name St. 2nd line 3rd line", + secondary: "1-000-000-0000", + }), + image: "", + }, + ], + }, + }, + { + description: "Focus on an `address-line1` field", + options: {}, + matchingProfiles, + allFieldNames, + searchString: "", + fieldName: "address-line1", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + defaultIndex: 0, + items: [ + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[0]), + label: JSON.stringify({ + primary: "123 Sesame Street.", + secondary: "Timothy Berners-Lee", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[1]), + label: JSON.stringify({ + primary: "331 E. Evelyn Avenue", + secondary: "John Doe", + }), + image: "", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[2]), + label: JSON.stringify({ + primary: "321, No Name St.", + secondary: "1-000-000-0000", + }), + image: "", + }, + ], + }, + }, + { + description: "No matching profiles", + options: {}, + matchingProfiles: [], + allFieldNames, + searchString: "", + fieldName: "", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH, + defaultIndex: 0, + items: [], + }, + }, + { + description: "Search with failure", + options: { resultCode: Ci.nsIAutoCompleteResult.RESULT_FAILURE }, + matchingProfiles: [], + allFieldNames, + searchString: "", + fieldName: "", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + items: [], + }, + }, +]; + +matchingProfiles = [ + { + guid: "test-guid-1", + "cc-name": "Timothy Berners-Lee", + "cc-number": "************6785", + "cc-exp-month": 12, + "cc-exp-year": 2014, + "cc-type": "visa", + }, + { + guid: "test-guid-2", + "cc-name": "John Doe", + "cc-number": "************1234", + "cc-exp-month": 4, + "cc-exp-year": 2014, + "cc-type": "amex", + }, + { + guid: "test-guid-3", + "cc-number": "************5678", + "cc-exp-month": 8, + "cc-exp-year": 2018, + }, +]; + +allFieldNames = ["cc-name", "cc-number", "cc-exp-month", "cc-exp-year"]; + +let creditCardTestCases = [ + { + description: "Focus on a `cc-name` field", + options: {}, + matchingProfiles, + allFieldNames, + searchString: "", + fieldName: "cc-name", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + defaultIndex: 0, + items: [ + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[0]), + label: JSON.stringify({ + primary: "Timothy Berners-Lee", + secondary: "****6785", + ariaLabel: "Visa Timothy Berners-Lee ****6785", + }), + image: "chrome://formautofill/content/third-party/cc-logo-visa.svg", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[1]), + label: JSON.stringify({ + primary: "John Doe", + secondary: "****1234", + ariaLabel: "American Express John Doe ****1234", + }), + image: "chrome://formautofill/content/third-party/cc-logo-amex.png", + }, + ], + }, + }, + { + description: "Focus on a `cc-number` field", + options: {}, + matchingProfiles, + allFieldNames, + searchString: "", + fieldName: "cc-number", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + defaultIndex: 0, + items: [ + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[0]), + label: JSON.stringify({ + primaryAffix: "****", + primary: "6785", + secondary: "Timothy Berners-Lee", + ariaLabel: "Visa **** 6785 Timothy Berners-Lee", + }), + image: "chrome://formautofill/content/third-party/cc-logo-visa.svg", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[1]), + label: JSON.stringify({ + primaryAffix: "****", + primary: "1234", + secondary: "John Doe", + ariaLabel: "American Express **** 1234 John Doe", + }), + image: "chrome://formautofill/content/third-party/cc-logo-amex.png", + }, + { + value: "", + style: "autofill-profile", + comment: JSON.stringify(matchingProfiles[2]), + label: JSON.stringify({ + primaryAffix: "****", + primary: "5678", + secondary: "", + ariaLabel: "**** 5678", + }), + image: "chrome://formautofill/content/icon-credit-card-generic.svg", + }, + ], + }, + }, + { + description: "No matching profiles", + options: {}, + matchingProfiles: [], + allFieldNames, + searchString: "", + fieldName: "", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH, + defaultIndex: 0, + items: [], + }, + }, + { + description: "Search with failure", + options: { resultCode: Ci.nsIAutoCompleteResult.RESULT_FAILURE }, + matchingProfiles: [], + allFieldNames, + searchString: "", + fieldName: "", + expected: { + searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + items: [], + }, + }, +]; + +add_task(async function test_all_patterns() { + let testSets = [ + { + collectionConstructor: AddressResult, + testCases: addressTestCases, + }, + { + collectionConstructor: CreditCardResult, + testCases: creditCardTestCases, + }, + ]; + + testSets.forEach(({ collectionConstructor, testCases }) => { + testCases.forEach(testCase => { + info("Starting testcase: " + testCase.description); + let actual = new collectionConstructor( + testCase.searchString, + testCase.fieldName, + testCase.allFieldNames, + testCase.matchingProfiles, + testCase.options + ); + let expectedValue = testCase.expected; + let expectedItemLength = expectedValue.items.length; + // If the last item shows up as a footer, we expect one more item + // than expected. + if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") { + expectedItemLength++; + } + + equal(actual.searchResult, expectedValue.searchResult); + equal(actual.defaultIndex, expectedValue.defaultIndex); + equal(actual.matchCount, expectedItemLength); + expectedValue.items.forEach((item, index) => { + equal(actual.getValueAt(index), item.value); + equal(actual.getCommentAt(index), item.comment); + equal(actual.getLabelAt(index), item.label); + equal(actual.getStyleAt(index), item.style); + equal(actual.getImageAt(index), item.image); + }); + + if (expectedValue.items.length) { + Assert.throws( + () => actual.getValueAt(expectedItemLength), + /Index out of range\./ + ); + + Assert.throws( + () => actual.getLabelAt(expectedItemLength), + /Index out of range\./ + ); + + Assert.throws( + () => actual.getCommentAt(expectedItemLength), + /Index out of range\./ + ); + } + }); + }); +}); diff --git a/browser/extensions/formautofill/test/unit/test_reconcile.js b/browser/extensions/formautofill/test/unit/test_reconcile.js new file mode 100644 index 0000000000..1700f89fe3 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_reconcile.js @@ -0,0 +1,1173 @@ +"use strict"; + +const TEST_STORE_FILE_NAME = "test-profile.json"; +const { CREDIT_CARD_SCHEMA_VERSION } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorageBase.sys.mjs" +); + +// NOTE: a guide to reading these test-cases: +// parent: What the local record looked like the last time we wrote the +// record to the Sync server. +// local: What the local record looks like now. IOW, the differences between +// 'parent' and 'local' are changes recently made which we wish to sync. +// remote: An incoming record we need to apply (ie, a record that was possibly +// changed on a remote device) +// +// To further help understanding this, a few of the testcases are annotated. +const ADDRESS_RECONCILE_TESTCASES = [ + { + description: "Local change", + parent: { + // So when we last wrote the record to the server, it had these values. + guid: "2bbd2d8fbc6b", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + local: [ + { + // The current local record - by comparing against parent we can see that + // only the given-name has changed locally. + "given-name": "Skip", + "family-name": "Hammond", + }, + ], + remote: { + // This is the incoming record. It has the same values as "parent", so + // we can deduce the record hasn't actually been changed remotely so we + // can safely ignore the incoming record and write our local changes. + guid: "2bbd2d8fbc6b", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + reconciled: { + guid: "2bbd2d8fbc6b", + "given-name": "Skip", + "family-name": "Hammond", + }, + }, + { + description: "Remote change", + parent: { + guid: "e3680e9f890d", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + }, + ], + remote: { + guid: "e3680e9f890d", + version: 1, + "given-name": "Skip", + "family-name": "Hammond", + }, + reconciled: { + guid: "e3680e9f890d", + "given-name": "Skip", + "family-name": "Hammond", + }, + }, + { + description: "New local field", + parent: { + guid: "0cba738b1be0", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + ], + remote: { + guid: "0cba738b1be0", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + reconciled: { + guid: "0cba738b1be0", + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + }, + { + description: "New remote field", + parent: { + guid: "be3ef97f8285", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + }, + ], + remote: { + guid: "be3ef97f8285", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + reconciled: { + guid: "be3ef97f8285", + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + }, + { + description: "Deleted field locally", + parent: { + guid: "9627322248ec", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + }, + ], + remote: { + guid: "9627322248ec", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + reconciled: { + guid: "9627322248ec", + "given-name": "Mark", + "family-name": "Hammond", + }, + }, + { + description: "Deleted field remotely", + parent: { + guid: "7d7509f3eeb2", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + ], + remote: { + guid: "7d7509f3eeb2", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + reconciled: { + guid: "7d7509f3eeb2", + "given-name": "Mark", + "family-name": "Hammond", + }, + }, + { + description: "Local and remote changes to unrelated fields", + parent: { + // The last time we wrote this to the server, country was NZ. + guid: "e087a06dfc57", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + country: "NZ", + // We also had an unknown field we round-tripped + foo: "bar", + }, + local: [ + { + // The current local record - so locally we've changed given-name to Skip. + "given-name": "Skip", + "family-name": "Hammond", + country: "NZ", + }, + ], + remote: { + // Remotely, we've changed the country to AU. + guid: "e087a06dfc57", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + country: "AU", + // This is a new unknown field that should send instead! + "unknown-1": "an unknown field from another client", + }, + reconciled: { + guid: "e087a06dfc57", + "given-name": "Skip", + "family-name": "Hammond", + country: "AU", + // This is a new unknown field that should send instead! + "unknown-1": "an unknown field from another client", + }, + }, + { + description: "Multiple local changes", + parent: { + guid: "340a078c596f", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + }, + local: [ + { + "given-name": "Skip", + "family-name": "Hammond", + }, + { + "given-name": "Skip", + "family-name": "Hammond", + organization: "Mozilla", + }, + ], + remote: { + guid: "340a078c596f", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + country: "AU", + }, + reconciled: { + guid: "340a078c596f", + "given-name": "Skip", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + }, + }, + { + // Local and remote diverged from the shared parent, but the values are the + // same, so we shouldn't fork. + description: "Same change to local and remote", + parent: { + guid: "0b3a72a1bea2", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + // unknown fields we previously roundtripped + foo: "bar", + }, + local: [ + { + "given-name": "Skip", + "family-name": "Hammond", + }, + ], + remote: { + guid: "0b3a72a1bea2", + version: 1, + "given-name": "Skip", + "family-name": "Hammond", + // New unknown field that should be the new round trip + "unknown-1": "an unknown field from another client", + }, + reconciled: { + guid: "0b3a72a1bea2", + "given-name": "Skip", + "family-name": "Hammond", + }, + }, + { + description: "Conflicting changes to single field", + parent: { + // This is what we last wrote to the sync server. + guid: "62068784d089", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + "unknown-1": "an unknown field from another client", + }, + local: [ + { + // The current version of the local record - the given-name has changed locally. + "given-name": "Skip", + "family-name": "Hammond", + }, + ], + remote: { + // An incoming record has a different given-name than any of the above! + guid: "62068784d089", + version: 1, + "given-name": "Kip", + "family-name": "Hammond", + "unknown-1": "an unknown field from another client", + }, + forked: { + // So we've forked the local record to a new GUID (and the next sync is + // going to write this as a new record) + "given-name": "Skip", + "family-name": "Hammond", + "unknown-1": "an unknown field from another client", + }, + reconciled: { + // And we've updated the local version of the record to be the remote version. + guid: "62068784d089", + "given-name": "Kip", + "family-name": "Hammond", + "unknown-1": "an unknown field from another client", + }, + }, + { + description: "Conflicting changes to multiple fields", + parent: { + guid: "244dbb692e94", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + country: "NZ", + }, + local: [ + { + "given-name": "Skip", + "family-name": "Hammond", + country: "AU", + }, + ], + remote: { + guid: "244dbb692e94", + version: 1, + "given-name": "Kip", + "family-name": "Hammond", + country: "CA", + }, + forked: { + "given-name": "Skip", + "family-name": "Hammond", + country: "AU", + }, + reconciled: { + guid: "244dbb692e94", + "given-name": "Kip", + "family-name": "Hammond", + country: "CA", + }, + }, + { + description: "Field deleted locally, changed remotely", + parent: { + guid: "6fc45e03d19a", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + country: "AU", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + }, + ], + remote: { + guid: "6fc45e03d19a", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + country: "NZ", + }, + forked: { + "given-name": "Mark", + "family-name": "Hammond", + }, + reconciled: { + guid: "6fc45e03d19a", + "given-name": "Mark", + "family-name": "Hammond", + country: "NZ", + }, + }, + { + description: "Field changed locally, deleted remotely", + parent: { + guid: "fff9fa27fa18", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + country: "AU", + }, + local: [ + { + "given-name": "Mark", + "family-name": "Hammond", + country: "NZ", + }, + ], + remote: { + guid: "fff9fa27fa18", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + }, + forked: { + "given-name": "Mark", + "family-name": "Hammond", + country: "NZ", + }, + reconciled: { + guid: "fff9fa27fa18", + "given-name": "Mark", + "family-name": "Hammond", + }, + }, + { + // Created, last modified should be synced; last used and times used should + // be local. Remote created time older than local, remote modified time + // newer than local. + description: + "Created, last modified time reconciliation without local changes", + parent: { + guid: "5113f329c42f", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + timeCreated: 1234, + timeLastModified: 5678, + timeLastUsed: 5678, + timesUsed: 6, + }, + local: [], + remote: { + guid: "5113f329c42f", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + timeCreated: 1200, + timeLastModified: 5700, + timeLastUsed: 5700, + timesUsed: 3, + }, + reconciled: { + guid: "5113f329c42f", + "given-name": "Mark", + "family-name": "Hammond", + timeCreated: 1200, + timeLastModified: 5700, + timeLastUsed: 5678, + timesUsed: 6, + }, + }, + { + // Local changes, remote created time newer than local, remote modified time + // older than local. + description: + "Created, last modified time reconciliation with local changes", + parent: { + guid: "791e5608b80a", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + timeCreated: 1234, + timeLastModified: 5678, + timeLastUsed: 5678, + timesUsed: 6, + }, + local: [ + { + "given-name": "Skip", + "family-name": "Hammond", + }, + ], + remote: { + guid: "791e5608b80a", + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + timeCreated: 1300, + timeLastModified: 5000, + timeLastUsed: 5000, + timesUsed: 3, + }, + reconciled: { + guid: "791e5608b80a", + "given-name": "Skip", + "family-name": "Hammond", + timeCreated: 1234, + timeLastUsed: 5678, + timesUsed: 6, + }, + }, +]; + +const CREDIT_CARD_RECONCILE_TESTCASES = [ + { + description: "Local change", + parent: { + // So when we last wrote the record to the server, it had these values. + guid: "2bbd2d8fbc6b", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "unknown-1": "an unknown field from another client", + }, + local: [ + { + // The current local record - by comparing against parent we can see that + // only the cc-number has changed locally. + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + ], + remote: { + // This is the incoming record. It has the same values as "parent", so + // we can deduce the record hasn't actually been changed remotely so we + // can safely ignore the incoming record and write our local changes. + guid: "2bbd2d8fbc6b", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "unknown-2": "a newer unknown field", + }, + reconciled: { + guid: "2bbd2d8fbc6b", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "unknown-2": "a newer unknown field", + }, + }, + { + description: "Remote change", + parent: { + guid: "e3680e9f890d", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "unknown-1": "unknown field", + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + ], + remote: { + guid: "e3680e9f890d", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "unknown-1": "unknown field", + }, + reconciled: { + guid: "e3680e9f890d", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "unknown-1": "unknown field", + }, + }, + + { + description: "New local field", + parent: { + guid: "0cba738b1be0", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + ], + remote: { + guid: "0cba738b1be0", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + reconciled: { + guid: "0cba738b1be0", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + }, + { + description: "New remote field", + parent: { + guid: "be3ef97f8285", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + ], + remote: { + guid: "be3ef97f8285", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + reconciled: { + guid: "be3ef97f8285", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + }, + { + description: "Deleted field locally", + parent: { + guid: "9627322248ec", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + ], + remote: { + guid: "9627322248ec", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + reconciled: { + guid: "9627322248ec", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + }, + { + description: "Deleted field remotely", + parent: { + guid: "7d7509f3eeb2", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + ], + remote: { + guid: "7d7509f3eeb2", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + reconciled: { + guid: "7d7509f3eeb2", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + }, + { + description: "Local and remote changes to unrelated fields", + parent: { + // The last time we wrote this to the server, "cc-exp-month" was 12. + guid: "e087a06dfc57", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + "unknown-1": "unknown field", + }, + local: [ + { + // The current local record - so locally we've changed "cc-number". + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 12, + }, + ], + remote: { + // Remotely, we've changed "cc-exp-month" to 1. + guid: "e087a06dfc57", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 1, + "unknown-2": "a newer unknown field", + }, + reconciled: { + guid: "e087a06dfc57", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 1, + "unknown-2": "a newer unknown field", + }, + }, + { + description: "Multiple local changes", + parent: { + guid: "340a078c596f", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "unknown-1": "unknown field", + }, + local: [ + { + "cc-name": "Skip", + "cc-number": "4111111111111111", + }, + { + "cc-name": "Skip", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + ], + remote: { + guid: "340a078c596f", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-year": 2000, + "unknown-1": "unknown field", + }, + reconciled: { + guid: "340a078c596f", + "cc-name": "Skip", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + "cc-exp-year": 2000, + "unknown-1": "unknown field", + }, + }, + { + // Local and remote diverged from the shared parent, but the values are the + // same, so we shouldn't fork. + description: "Same change to local and remote", + parent: { + guid: "0b3a72a1bea2", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + ], + remote: { + guid: "0b3a72a1bea2", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + reconciled: { + guid: "0b3a72a1bea2", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + }, + { + description: "Conflicting changes to single field", + parent: { + // This is what we last wrote to the sync server. + guid: "62068784d089", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "unknown-1": "unknown field", + }, + local: [ + { + // The current version of the local record - the cc-number has changed locally. + "cc-name": "John Doe", + "cc-number": "5103059495477870", + }, + ], + remote: { + // An incoming record has a different cc-number than any of the above! + guid: "62068784d089", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "unknown-1": "unknown field", + }, + forked: { + // So we've forked the local record to a new GUID (and the next sync is + // going to write this as a new record) + "cc-name": "John Doe", + "cc-number": "5103059495477870", + "unknown-1": "unknown field", + }, + reconciled: { + // And we've updated the local version of the record to be the remote version. + guid: "62068784d089", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "unknown-1": "unknown field", + }, + }, + { + description: "Conflicting changes to multiple fields", + parent: { + guid: "244dbb692e94", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "5103059495477870", + "cc-exp-month": 1, + }, + ], + remote: { + guid: "244dbb692e94", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 3, + }, + forked: { + "cc-name": "John Doe", + "cc-number": "5103059495477870", + "cc-exp-month": 1, + }, + reconciled: { + guid: "244dbb692e94", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + "cc-exp-month": 3, + }, + }, + { + description: "Field deleted locally, changed remotely", + parent: { + guid: "6fc45e03d19a", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + ], + remote: { + guid: "6fc45e03d19a", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 3, + }, + forked: { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + reconciled: { + guid: "6fc45e03d19a", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 3, + }, + }, + { + description: "Field changed locally, deleted remotely", + parent: { + guid: "fff9fa27fa18", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 12, + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 3, + }, + ], + remote: { + guid: "fff9fa27fa18", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + forked: { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 3, + }, + reconciled: { + guid: "fff9fa27fa18", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + }, + }, + { + // Created, last modified should be synced; last used and times used should + // be local. Remote created time older than local, remote modified time + // newer than local. + description: + "Created, last modified time reconciliation without local changes", + parent: { + guid: "5113f329c42f", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + timeCreated: 1234, + timeLastModified: 5678, + timeLastUsed: 5678, + timesUsed: 6, + }, + local: [], + remote: { + guid: "5113f329c42f", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + timeCreated: 1200, + timeLastModified: 5700, + timeLastUsed: 5700, + timesUsed: 3, + }, + reconciled: { + guid: "5113f329c42f", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + timeCreated: 1200, + timeLastModified: 5700, + timeLastUsed: 5678, + timesUsed: 6, + }, + }, + { + // Local changes, remote created time newer than local, remote modified time + // older than local. + description: + "Created, last modified time reconciliation with local changes", + parent: { + guid: "791e5608b80a", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + timeCreated: 1234, + timeLastModified: 5678, + timeLastUsed: 5678, + timesUsed: 6, + }, + local: [ + { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + ], + remote: { + guid: "791e5608b80a", + version: CREDIT_CARD_SCHEMA_VERSION, + "cc-name": "John Doe", + "cc-number": "4111111111111111", + timeCreated: 1300, + timeLastModified: 5000, + timeLastUsed: 5000, + timesUsed: 3, + }, + reconciled: { + guid: "791e5608b80a", + "cc-name": "John Doe", + "cc-number": "4929001587121045", + timeCreated: 1234, + timeLastUsed: 5678, + timesUsed: 6, + }, + }, +]; + +add_task(async function test_reconcile_unknown_version() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + + // Cross-version reconciliation isn't supported yet. See bug 1377204. + await Assert.rejects( + profileStorage.addresses.reconcile({ + guid: "31d83d2725ec", + version: 3, + "given-name": "Mark", + "family-name": "Hammond", + }), + /Got unknown record version/ + ); +}); + +add_task(async function test_reconcile_idempotent() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + + let guid = "de1ba7b094fe"; + await profileStorage.addresses.add( + { + guid, + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + // an unknown field from a previous sync + foo: "bar", + }, + { sourceSync: true } + ); + await profileStorage.addresses.update(guid, { + "given-name": "Skip", + "family-name": "Hammond", + organization: "Mozilla", + }); + + let remote = { + guid, + version: 1, + "given-name": "Mark", + "family-name": "Hammond", + tel: "123456", + "unknown-1": "an unknown field from another client", + }; + + { + let { forkedGUID } = await profileStorage.addresses.reconcile(remote); + let updatedRecord = await profileStorage.addresses.get(guid, { + rawData: true, + }); + + ok(!forkedGUID, "First merge should not fork record"); + ok( + objectMatches(updatedRecord, { + guid: "de1ba7b094fe", + "given-name": "Skip", + "family-name": "Hammond", + organization: "Mozilla", + tel: "123456", + "unknown-1": "an unknown field from another client", + }), + "First merge should merge local and remote changes" + ); + } + + { + let { forkedGUID } = await profileStorage.addresses.reconcile(remote); + let updatedRecord = await profileStorage.addresses.get(guid, { + rawData: true, + }); + + ok(!forkedGUID, "Second merge should not fork record"); + ok( + objectMatches(updatedRecord, { + guid: "de1ba7b094fe", + "given-name": "Skip", + "family-name": "Hammond", + organization: "Mozilla", + tel: "123456", + "unknown-1": "an unknown field from another client", + }), + "Second merge should not change record" + ); + } +}); + +add_task(async function test_reconcile_three_way_merge() { + let TESTCASES = { + addresses: ADDRESS_RECONCILE_TESTCASES, + creditCards: CREDIT_CARD_RECONCILE_TESTCASES, + }; + + for (let collectionName in TESTCASES) { + info(`Start to test reconcile on ${collectionName}`); + + let profileStorage = await initProfileStorage( + TEST_STORE_FILE_NAME, + null, + collectionName + ); + + for (let test of TESTCASES[collectionName]) { + info(test.description); + + await profileStorage[collectionName].add(test.parent, { + sourceSync: true, + }); + + for (let updatedRecord of test.local) { + await profileStorage[collectionName].update( + test.parent.guid, + updatedRecord + ); + } + + let localRecord = await profileStorage[collectionName].get( + test.parent.guid, + { + rawData: true, + } + ); + + let onReconciled = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => + data == "reconcile" && + subject.wrappedJSObject.collectionName == collectionName + ); + let { forkedGUID } = await profileStorage[collectionName].reconcile( + test.remote + ); + await onReconciled; + let reconciledRecord = await profileStorage[collectionName].get( + test.parent.guid, + { + rawData: true, + } + ); + if (forkedGUID) { + let forkedRecord = await profileStorage[collectionName].get( + forkedGUID, + { + rawData: true, + } + ); + + notEqual(forkedRecord.guid, reconciledRecord.guid); + equal(forkedRecord.timeLastModified, localRecord.timeLastModified); + ok( + objectMatches(forkedRecord, test.forked), + `${test.description} should fork record` + ); + } else { + ok(!test.forked, `${test.description} should not fork record`); + } + + ok(objectMatches(reconciledRecord, test.reconciled)); + } + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_savedFieldNames.js b/browser/extensions/formautofill/test/unit/test_savedFieldNames.js new file mode 100644 index 0000000000..6e3474c06d --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_savedFieldNames.js @@ -0,0 +1,106 @@ +/* + * Test for keeping the valid fields information in sharedData. + */ + +"use strict"; + +let FormAutofillStatus; + +add_setup(async () => { + ({ FormAutofillStatus } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillParent.sys.mjs" + )); +}); + +add_task(async function test_profileSavedFieldNames_init() { + FormAutofillStatus.init(); + sinon.stub(FormAutofillStatus, "updateSavedFieldNames"); + + await FormAutofillStatus.formAutofillStorage.initialize(); + Assert.equal(FormAutofillStatus.updateSavedFieldNames.called, true); + + FormAutofillStatus.uninit(); +}); + +add_task(async function test_profileSavedFieldNames_observe() { + FormAutofillStatus.init(); + + // profile changed => Need to trigger updateValidFields + ["add", "update", "remove", "reconcile", "removeAll"].forEach(event => { + FormAutofillStatus.observe(null, "formautofill-storage-changed", event); + Assert.equal(FormAutofillStatus.updateSavedFieldNames.called, true); + }); + + // profile metadata updated => no need to trigger updateValidFields + FormAutofillStatus.updateSavedFieldNames.resetHistory(); + FormAutofillStatus.observe( + null, + "formautofill-storage-changed", + "notifyUsed" + ); + Assert.equal(FormAutofillStatus.updateSavedFieldNames.called, false); + FormAutofillStatus.updateSavedFieldNames.restore(); +}); + +add_task(async function test_profileSavedFieldNames_update() { + registerCleanupFunction(function cleanup() { + Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); + }); + + Object.defineProperty( + FormAutofillStatus.formAutofillStorage.addresses, + "_data", + { writable: true } + ); + + FormAutofillStatus.formAutofillStorage.addresses._data = []; + + // The set is empty if there's no profile in the store. + await FormAutofillStatus.updateSavedFieldNames(); + Assert.equal( + Services.ppmm.sharedData.get("FormAutofill:savedFieldNames").size, + 0 + ); + + // 2 profiles with 4 valid fields. + FormAutofillStatus.formAutofillStorage.addresses._data = [ + { + guid: "test-guid-1", + organization: "Sesame Street", + "street-address": "123 Sesame Street.", + tel: "1-345-345-3456", + email: "", + timeCreated: 0, + timeLastUsed: 0, + timeLastModified: 0, + timesUsed: 0, + }, + { + guid: "test-guid-2", + organization: "Mozilla", + "street-address": "331 E. Evelyn Avenue", + tel: "1-650-903-0800", + country: "US", + timeCreated: 0, + timeLastUsed: 0, + timeLastModified: 0, + timesUsed: 0, + }, + ]; + + await FormAutofillStatus.updateSavedFieldNames(); + + let autofillSavedFieldNames = Services.ppmm.sharedData.get( + "FormAutofill:savedFieldNames" + ); + Assert.equal(autofillSavedFieldNames.size, 4); + Assert.equal(autofillSavedFieldNames.has("organization"), true); + Assert.equal(autofillSavedFieldNames.has("street-address"), true); + Assert.equal(autofillSavedFieldNames.has("tel"), true); + Assert.equal(autofillSavedFieldNames.has("email"), false); + Assert.equal(autofillSavedFieldNames.has("guid"), false); + Assert.equal(autofillSavedFieldNames.has("timeCreated"), false); + Assert.equal(autofillSavedFieldNames.has("timeLastUsed"), false); + Assert.equal(autofillSavedFieldNames.has("timeLastModified"), false); + Assert.equal(autofillSavedFieldNames.has("timesUsed"), false); +}); diff --git a/browser/extensions/formautofill/test/unit/test_storage_remove.js b/browser/extensions/formautofill/test/unit/test_storage_remove.js new file mode 100644 index 0000000000..0c33440ea9 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_storage_remove.js @@ -0,0 +1,88 @@ +/** + * Tests removing all address/creditcard records. + */ + +"use strict"; + +let FormAutofillStorage; +add_setup(async () => { + ({ FormAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + )); +}); + +const TEST_STORE_FILE_NAME = "test-tombstones.json"; + +const TEST_ADDRESS_1 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+1 617 253 5702", + email: "timbl@w3.org", +}; + +const TEST_ADDRESS_2 = { + "street-address": "Some Address", + country: "US", +}; + +const TEST_CREDIT_CARD_1 = { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 4, + "cc-exp-year": 2017, +}; + +const TEST_CREDIT_CARD_2 = { + "cc-name": "Timothy Berners-Lee", + "cc-number": "4929001587121045", + "cc-exp-month": 12, + "cc-exp-year": 2022, +}; + +// Like add_task, but actually adds 2 - one for addresses and one for cards. +function add_storage_task(test_function) { + add_task(async function () { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + let address_records = [TEST_ADDRESS_1, TEST_ADDRESS_2]; + let cc_records = [TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2]; + + for (let [storage, record] of [ + [profileStorage.addresses, address_records], + [profileStorage.creditCards, cc_records], + ]) { + await test_function(storage, record); + } + }); +} + +add_storage_task(async function test_remove_everything(storage, records) { + info("check simple tombstone semantics"); + + let guid = await storage.add(records[0]); + Assert.equal((await storage.getAll()).length, 1); + + storage.pullSyncChanges(); // force sync metadata, which triggers tombstone behaviour. + + storage.remove(guid); + + await storage.add(records[1]); + // getAll() is still 1 as we deleted the first. + Assert.equal((await storage.getAll()).length, 1); + + // check we have the tombstone. + Assert.equal((await storage.getAll({ includeDeleted: true })).length, 2); + + storage.removeAll(); + + // should have deleted both the existing and deleted records. + Assert.equal((await storage.getAll({ includeDeleted: true })).length, 0); +}); diff --git a/browser/extensions/formautofill/test/unit/test_storage_syncfields.js b/browser/extensions/formautofill/test/unit/test_storage_syncfields.js new file mode 100644 index 0000000000..e304aa4df0 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_storage_syncfields.js @@ -0,0 +1,498 @@ +/** + * Tests FormAutofillStorage objects support for sync related fields. + */ + +"use strict"; + +// The duplication of some of these fixtures between tests is unfortunate. +const TEST_STORE_FILE_NAME = "test-profile.json"; + +const TEST_ADDRESS_1 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+1 617 253 5702", + email: "timbl@w3.org", + "unknown-1": "an unknown field we roundtrip", +}; + +const TEST_ADDRESS_2 = { + "street-address": "Some Address", + country: "US", +}; + +const TEST_ADDRESS_3 = { + "street-address": "Other Address", + "postal-code": "12345", +}; + +// storage.get() doesn't support getting deleted items. However, this test +// wants to do that, so rather than making .get() support that just for this +// test, we use this helper. +async function findGUID(storage, guid, options) { + let all = await storage.getAll(options); + let records = all.filter(r => r.guid == guid); + equal(records.length, 1); + return records[0]; +} + +add_task(async function test_changeCounter() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + + let [address] = await profileStorage.addresses.getAll(); + // new records don't get the sync metadata. + equal(getSyncChangeCounter(profileStorage.addresses, address.guid), -1); + // But we can force one. + profileStorage.addresses.pullSyncChanges(); + equal(getSyncChangeCounter(profileStorage.addresses, address.guid), 1); +}); + +add_task(async function test_pushChanges() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + profileStorage.addresses.pullSyncChanges(); // force sync metadata for all items + + let [, address] = await profileStorage.addresses.getAll(); + let guid = address.guid; + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + + // Pretend we're doing a sync now, and an update occured mid-sync. + let changes = { + [guid]: { + profile: address, + counter: changeCounter, + modified: address.timeLastModified, + synced: true, + }, + }; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == "update" + ); + await profileStorage.addresses.update(guid, TEST_ADDRESS_3); + await onChanged; + + changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + Assert.equal(changeCounter, 2); + + profileStorage.addresses.pushSyncChanges(changes); + address = await profileStorage.addresses.get(guid); + changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + + // Counter should still be 1, since our sync didn't record the mid-sync change + Assert.equal( + changeCounter, + 1, + "Counter shouldn't be zero because it didn't record update" + ); + + // now, push a new set of changes, which should make the changeCounter 0 + profileStorage.addresses.pushSyncChanges({ + [guid]: { + profile: address, + counter: changeCounter, + modified: address.timeLastModified, + synced: true, + }, + }); + + changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + Assert.equal(changeCounter, 0); +}); + +async function checkingSyncChange(action, callback) { + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == action + ); + await callback(); + let [subject] = await onChanged; + ok( + subject.wrappedJSObject.sourceSync, + "change notification should have source sync" + ); +} + +add_task(async function test_add_sourceSync() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + // Hardcode a guid so that we don't need to generate a dynamic regex + let guid = "aaaaaaaaaaaa"; + let testAddr = Object.assign({ guid, version: 1 }, TEST_ADDRESS_1); + + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, 0); + + await Assert.rejects( + profileStorage.addresses.add({ guid, deleted: true }, { sourceSync: true }), + /Record aaaaaaaaaaaa already exists/ + ); +}); + +add_task(async function test_add_tombstone_sourceSync() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + let testAddr = { guid, deleted: true }; + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + let added = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + ok(added); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); + ok(added.deleted); + + // Adding same record again shouldn't throw (or change anything) + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + added = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); + ok(added.deleted); +}); + +add_task(async function test_add_resurrects_tombstones() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + + // Add a tombstone. + await profileStorage.addresses.add({ guid, deleted: true }); + + // You can't re-add an item with an explicit GUID. + let resurrected = Object.assign({}, TEST_ADDRESS_1, { guid, version: 1 }); + await Assert.rejects( + profileStorage.addresses.add(resurrected), + /"(guid|version)" is not a valid field/ + ); + + // But Sync can! + let guid3 = await profileStorage.addresses.add(resurrected, { + sourceSync: true, + }); + equal(guid, guid3); + + let got = await profileStorage.addresses.get(guid); + equal(got["given-name"], TEST_ADDRESS_1["given-name"]); +}); + +add_task(async function test_remove_sourceSync_localChanges() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + profileStorage.addresses.pullSyncChanges(); // force sync metadata + + let [{ guid }] = await profileStorage.addresses.getAll(); + + equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + // try and remove a record stored locally with local changes + await checkingSyncChange("remove", async () => + profileStorage.addresses.remove(guid, { sourceSync: true }) + ); + + let record = await profileStorage.addresses.get(guid); + ok(record); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); +}); + +add_task(async function test_remove_sourceSync_unknown() { + // remove a record not stored locally + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + await checkingSyncChange("remove", async () => + profileStorage.addresses.remove(guid, { sourceSync: true }) + ); + + let tombstone = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + ok(tombstone.deleted); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); +}); + +add_task(async function test_remove_sourceSync_unchanged() { + // Remove a local record without a change counter. + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + let addr = Object.assign({ guid, version: 1 }, TEST_ADDRESS_1); + // add a record with sourceSync to guarantee changeCounter == 0 + await checkingSyncChange("add", async () => + profileStorage.addresses.add(addr, { sourceSync: true }) + ); + + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); + + await checkingSyncChange("remove", async () => + profileStorage.addresses.remove(guid, { sourceSync: true }) + ); + + let tombstone = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + ok(tombstone.deleted); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); +}); + +add_task(async function test_pullSyncChanges() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let startAddresses = await profileStorage.addresses.getAll(); + equal(startAddresses.length, 2); + // All should start without sync metadata + for (let { guid } of profileStorage.addresses._store.data.addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, -1); + } + profileStorage.addresses.pullSyncChanges(); // force sync metadata + + let addedDirectGUID = profileStorage.addresses._generateGUID(); + let testAddr = Object.assign( + { guid: addedDirectGUID, version: 1 }, + TEST_ADDRESS_1, + TEST_ADDRESS_3 + ); + + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + let tombstoneGUID = profileStorage.addresses._generateGUID(); + await checkingSyncChange("add", async () => + profileStorage.addresses.add( + { guid: tombstoneGUID, deleted: true }, + { sourceSync: true } + ) + ); + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == "remove" + ); + + profileStorage.addresses.remove(startAddresses[0].guid); + await onChanged; + + let addresses = await profileStorage.addresses.getAll({ + includeDeleted: true, + }); + + // Should contain changes with a change counter + let changes = profileStorage.addresses.pullSyncChanges(); + equal(Object.keys(changes).length, 2); + + ok(changes[startAddresses[0].guid].profile.deleted); + equal(changes[startAddresses[0].guid].counter, 2); + + ok(!changes[startAddresses[1].guid].profile.deleted); + equal(changes[startAddresses[1].guid].counter, 1); + + ok( + !changes[tombstoneGUID], + "Missing because it's a tombstone from sourceSync" + ); + ok(!changes[addedDirectGUID], "Missing because it was added with sourceSync"); + + for (let address of addresses) { + let change = changes[address.guid]; + if (!change) { + continue; + } + equal(change.profile.guid, address.guid); + let changeCounter = getSyncChangeCounter( + profileStorage.addresses, + change.profile.guid + ); + equal(change.counter, changeCounter); + ok(!change.synced); + } +}); + +add_task(async function test_pullPushChanges() { + // round-trip changes between pull and push + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + let psa = profileStorage.addresses; + + let guid1 = await psa.add(TEST_ADDRESS_1); + let guid2 = await psa.add(TEST_ADDRESS_2); + let guid3 = await psa.add(TEST_ADDRESS_3); + + let changes = psa.pullSyncChanges(); + + equal(getSyncChangeCounter(psa, guid1), 1); + equal(getSyncChangeCounter(psa, guid2), 1); + equal(getSyncChangeCounter(psa, guid3), 1); + + // between the pull and the push we change the second. + await psa.update(guid2, Object.assign({}, TEST_ADDRESS_2, { country: "AU" })); + equal(getSyncChangeCounter(psa, guid2), 2); + // and update the changeset to indicated we did update the first 2, but failed + // to update the 3rd for some reason. + changes[guid1].synced = true; + changes[guid2].synced = true; + + psa.pushSyncChanges(changes); + + // first was synced correctly. + equal(getSyncChangeCounter(psa, guid1), 0); + // second was synced correctly, but it had a change while syncing. + equal(getSyncChangeCounter(psa, guid2), 1); + // 3rd wasn't marked as having synced. + equal(getSyncChangeCounter(psa, guid3), 1); +}); + +add_task(async function test_changeGUID() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let newguid = () => profileStorage.addresses._generateGUID(); + + let guid_synced = await profileStorage.addresses.add(TEST_ADDRESS_1); + + // pullSyncChanges so guid_synced is flagged as syncing. + profileStorage.addresses.pullSyncChanges(); + + // and 2 items that haven't been synced. + let guid_u1 = await profileStorage.addresses.add(TEST_ADDRESS_2); + let guid_u2 = await profileStorage.addresses.add(TEST_ADDRESS_3); + + // Change a non-existing guid + Assert.throws( + () => profileStorage.addresses.changeGUID(newguid(), newguid()), + /changeGUID: no source record/ + ); + // Change to a guid that already exists. + Assert.throws( + () => profileStorage.addresses.changeGUID(guid_u1, guid_u2), + /changeGUID: record with destination id exists already/ + ); + // Try and change a guid that's already been synced. + Assert.throws( + () => profileStorage.addresses.changeGUID(guid_synced, newguid()), + /changeGUID: existing record has already been synced/ + ); + + // Change an item to itself makes no sense. + Assert.throws( + () => profileStorage.addresses.changeGUID(guid_u1, guid_u1), + /changeGUID: old and new IDs are the same/ + ); + + // and one that works. + equal( + (await profileStorage.addresses.getAll({ includeDeleted: true })).length, + 3 + ); + let targetguid = newguid(); + profileStorage.addresses.changeGUID(guid_u1, targetguid); + equal( + (await profileStorage.addresses.getAll({ includeDeleted: true })).length, + 3 + ); + + ok( + await profileStorage.addresses.get(guid_synced), + "synced item still exists." + ); + ok( + await profileStorage.addresses.get(guid_u2), + "guid we didn't touch still exists." + ); + ok(await profileStorage.addresses.get(targetguid), "target guid exists."); + ok( + !(await profileStorage.addresses.get(guid_u1)), + "old guid no longer exists." + ); +}); + +add_task(async function test_findDuplicateGUID() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + + let [record] = await profileStorage.addresses.getAll({ rawData: true }); + await Assert.rejects( + profileStorage.addresses.findDuplicateGUID(record), + /Record \w+ already exists/, + "Should throw if the GUID already exists" + ); + + // Add a malformed record, passing `sourceSync` to work around the record + // normalization logic that would prevent this. + let timeLastModified = Date.now(); + let timeCreated = timeLastModified - 60 * 1000; + + await profileStorage.addresses.add( + { + guid: profileStorage.addresses._generateGUID(), + version: 1, + timeCreated, + timeLastModified, + }, + { sourceSync: true } + ); + + strictEqual( + await profileStorage.addresses.findDuplicateGUID({ + guid: profileStorage.addresses._generateGUID(), + version: 1, + timeCreated, + timeLastModified, + }), + null, + "Should ignore internal fields and malformed records" + ); +}); + +add_task(async function test_reset() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + // All should start without sync metadata + for (let { guid } of addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, -1); + } + // pullSyncChanges should create the metadata. + profileStorage.addresses.pullSyncChanges(); + addresses = await profileStorage.addresses.getAll(); + for (let { guid } of addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, 1); + } + // and resetSync should wipe it. + profileStorage.addresses.resetSync(); + addresses = await profileStorage.addresses.getAll(); + for (let { guid } of addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, -1); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_storage_tombstones.js b/browser/extensions/formautofill/test/unit/test_storage_tombstones.js new file mode 100644 index 0000000000..584dac8043 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_storage_tombstones.js @@ -0,0 +1,190 @@ +/** + * Tests tombstones in address/creditcard records. + */ + +"use strict"; + +let FormAutofillStorage; +add_setup(async () => { + ({ FormAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + )); +}); + +const TEST_STORE_FILE_NAME = "test-tombstones.json"; + +const TEST_ADDRESS_1 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+1 617 253 5702", + email: "timbl@w3.org", +}; + +const TEST_CC_1 = { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 4, + "cc-exp-year": 2017, +}; + +let do_check_tombstone_record = profile => { + Assert.ok(profile.deleted); + Assert.deepEqual( + Object.keys(profile).sort(), + ["guid", "timeLastModified", "deleted"].sort() + ); +}; + +// Like add_task, but actually adds 2 - one for addresses and one for cards. +function add_storage_task(test_function) { + add_task(async function () { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + let profileStorage = new FormAutofillStorage(path); + let testCC1 = Object.assign({}, TEST_CC_1); + await profileStorage.initialize(); + + for (let [storage, record] of [ + [profileStorage.addresses, TEST_ADDRESS_1], + [profileStorage.creditCards, testCC1], + ]) { + await test_function(storage, record); + } + }); +} + +add_storage_task(async function test_simple_tombstone(storage, record) { + info("check simple tombstone semantics"); + + let guid = await storage.add(record); + Assert.equal((await storage.getAll()).length, 1); + + storage.remove(guid); + + // should be unable to get it normally. + Assert.equal(await storage.get(guid), null); + // and getAll should also not return it. + Assert.equal((await storage.getAll()).length, 0); + + // but getAll allows us to access deleted items - but we didn't create + // a tombstone here, so even that will not get it. + let all = await storage.getAll({ includeDeleted: true }); + Assert.equal(all.length, 0); +}); + +add_storage_task(async function test_simple_synctombstone(storage, record) { + info("check simple tombstone semantics for synced records"); + + let guid = await storage.add(record); + Assert.equal((await storage.getAll()).length, 1); + + storage.pullSyncChanges(); // force sync metadata, which triggers tombstone behaviour. + + storage.remove(guid); + + // should be unable to get it normally. + Assert.equal(await storage.get(guid), null); + // and getAll should also not return it. + Assert.equal((await storage.getAll()).length, 0); + + // but getAll allows us to access deleted items. + let all = await storage.getAll({ includeDeleted: true }); + Assert.equal(all.length, 1); + + do_check_tombstone_record(all[0]); + + // a tombstone got from API should look exactly the same as it got from the + // disk (besides "_sync"). + let tombstoneInDisk = Object.assign( + {}, + storage._store.data[storage._collectionName][0] + ); + delete tombstoneInDisk._sync; + do_check_tombstone_record(tombstoneInDisk); +}); + +add_storage_task(async function test_add_tombstone(storage, record) { + info("Should be able to add a new tombstone"); + let guid = await storage.add({ guid: "test-guid-1", deleted: true }); + + // should be unable to get it normally. + Assert.equal(await storage.get(guid), null); + // and getAll should also not return it. + Assert.equal((await storage.getAll()).length, 0); + + // but getAll allows us to access deleted items. + let all = await storage.getAll({ rawData: true, includeDeleted: true }); + Assert.equal(all.length, 1); + + do_check_tombstone_record(all[0]); + + // a tombstone got from API should look exactly the same as it got from the + // disk (besides "_sync"). + let tombstoneInDisk = Object.assign( + {}, + storage._store.data[storage._collectionName][0] + ); + delete tombstoneInDisk._sync; + do_check_tombstone_record(tombstoneInDisk); +}); + +add_storage_task(async function test_add_tombstone_without_guid( + storage, + record +) { + info("Should not be able to add a new tombstone without specifying the guid"); + await Assert.rejects(storage.add({ deleted: true }), /Record missing GUID/); + Assert.equal((await storage.getAll({ includeDeleted: true })).length, 0); +}); + +add_storage_task(async function test_add_tombstone_existing_guid( + storage, + record +) { + info( + "Should not be able to add a new tombstone when a record with that ID exists" + ); + let guid = await storage.add(record); + await Assert.rejects( + storage.add({ guid, deleted: true }), + /a record with this GUID already exists/ + ); + + // same if the existing item is already a tombstone. + await storage.add({ guid: "test-guid-1", deleted: true }); + await Assert.rejects( + storage.add({ guid: "test-guid-1", deleted: true }), + /a record with this GUID already exists/ + ); +}); + +add_storage_task(async function test_update_tombstone(storage, record) { + info("Updating a tombstone should fail"); + let guid = await storage.add({ guid: "test-guid-1", deleted: true }); + await Assert.rejects(storage.update(guid, {}), /No matching record./); +}); + +add_storage_task(async function test_remove_existing_tombstone( + storage, + record +) { + info("Removing a record that's already a tombstone should be a no-op"); + let guid = await storage.add({ + guid: "test-guid-1", + deleted: true, + timeLastModified: 1234, + }); + + storage.remove(guid); + let all = await storage.getAll({ rawData: true, includeDeleted: true }); + Assert.equal(all.length, 1); + + do_check_tombstone_record(all[0]); + equal(all[0].timeLastModified, 1234); // should not be updated to now(). +}); diff --git a/browser/extensions/formautofill/test/unit/test_sync.js b/browser/extensions/formautofill/test/unit/test_sync.js new file mode 100644 index 0000000000..bc9467d7c9 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_sync.js @@ -0,0 +1,1017 @@ +/** + * Tests sync functionality. + */ + +/* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */ +/* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */ +/* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */ +/* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */ + +"use strict"; + +const { Service } = ChromeUtils.importESModule( + "resource://services-sync/service.sys.mjs" +); +const { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule( + "resource://services-sync/constants.sys.mjs" +); + +const { sanitizeStorageObject, AutofillRecord, AddressesEngine } = + ChromeUtils.importESModule("resource://autofill/FormAutofillSync.sys.mjs"); + +Services.prefs.setCharPref("extensions.formautofill.loglevel", "Trace"); +initTestLogging("Trace"); + +const TEST_STORE_FILE_NAME = "test-profile.json"; + +const TEST_PROFILE_1 = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+16172535702", + email: "timbl@w3.org", + // A field this client doesn't "understand" from another client + "unknown-1": "some unknown data from another client", +}; + +const TEST_PROFILE_2 = { + "street-address": "Some Address", + country: "US", +}; + +async function expectLocalProfiles(profileStorage, expected) { + let profiles = await profileStorage.addresses.getAll({ + rawData: true, + includeDeleted: true, + }); + expected.sort((a, b) => a.guid.localeCompare(b.guid)); + profiles.sort((a, b) => a.guid.localeCompare(b.guid)); + try { + deepEqual( + profiles.map(p => p.guid), + expected.map(p => p.guid) + ); + for (let i = 0; i < expected.length; i++) { + let thisExpected = expected[i]; + let thisGot = profiles[i]; + // always check "deleted". + equal(thisExpected.deleted, thisGot.deleted); + ok(objectMatches(thisGot, thisExpected)); + } + } catch (ex) { + info("Comparing expected profiles:"); + info(JSON.stringify(expected, undefined, 2)); + info("against actual profiles:"); + info(JSON.stringify(profiles, undefined, 2)); + throw ex; + } +} + +async function setup() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + // should always start with no profiles. + Assert.equal( + (await profileStorage.addresses.getAll({ includeDeleted: true })).length, + 0 + ); + + Services.prefs.setCharPref( + "services.sync.log.logger.engine.addresses", + "Trace" + ); + let engine = new AddressesEngine(Service); + await engine.initialize(); + // Avoid accidental automatic sync due to our own changes + Service.scheduler.syncThreshold = 10000000; + let syncID = await engine.resetLocalSyncID(); + let server = serverForUsers( + { foo: "password" }, + { + meta: { + global: { engines: { addresses: { version: engine.version, syncID } } }, + }, + addresses: {}, + } + ); + + Service.engineManager._engines.addresses = engine; + engine.enabled = true; + engine._store._storage = profileStorage.addresses; + + generateNewKeys(Service.collectionKeys); + + await SyncTestingInfrastructure(server); + + let collection = server.user("foo").collection("addresses"); + + return { profileStorage, server, collection, engine }; +} + +async function cleanup(server) { + let promiseStartOver = promiseOneObserver("weave:service:start-over:finish"); + await Service.startOver(); + await promiseStartOver; + await promiseStopServer(server); +} + +add_task(async function test_log_sanitization() { + let sanitized = sanitizeStorageObject(TEST_PROFILE_1); + // all strings have been mangled. + for (let key of Object.keys(TEST_PROFILE_1)) { + let val = TEST_PROFILE_1[key]; + if (typeof val == "string") { + notEqual(sanitized[key], val); + } + } + // And check that stringifying a sync record is sanitized. + let record = new AutofillRecord("collection", "some-id"); + record.entry = TEST_PROFILE_1; + let serialized = record.toString(); + // None of the string values should appear in the output. + for (let key of Object.keys(TEST_PROFILE_1)) { + let val = TEST_PROFILE_1[key]; + if (typeof val == "string") { + ok(!serialized.includes(val), `"${val}" shouldn't be in: ${serialized}`); + } + } +}); + +add_task(async function test_outgoing() { + let { profileStorage, server, collection, engine } = await setup(); + try { + equal(engine._tracker.score, 0); + let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1); + // And a deleted item. + let deletedGUID = profileStorage.addresses._generateGUID(); + await profileStorage.addresses.add({ guid: deletedGUID, deleted: true }); + + await expectLocalProfiles(profileStorage, [ + { + guid: existingGUID, + }, + { + guid: deletedGUID, + deleted: true, + }, + ]); + + await engine._tracker.asyncObserver.promiseObserversComplete(); + // The tracker should have a score recorded for the 2 additions we had. + equal(engine._tracker.score, SCORE_INCREMENT_XLARGE * 2); + + await engine.setLastSync(0); + await engine.sync(); + + Assert.equal(collection.count(), 2); + Assert.ok(collection.wbo(existingGUID)); + Assert.ok(collection.wbo(deletedGUID)); + + await expectLocalProfiles(profileStorage, [ + { + guid: existingGUID, + }, + { + guid: deletedGUID, + deleted: true, + }, + ]); + + strictEqual( + getSyncChangeCounter(profileStorage.addresses, existingGUID), + 0 + ); + strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedGUID), 0); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_incoming_new() { + let { profileStorage, server, engine } = await setup(); + try { + let profileID = Utils.makeGUID(); + let deletedID = Utils.makeGUID(); + + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + profileID, + encryptPayload({ + id: profileID, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + getDateForSync() + ) + ); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + deletedID, + encryptPayload({ + id: deletedID, + deleted: true, + }), + getDateForSync() + ) + ); + + // The tracker should start with no score. + equal(engine._tracker.score, 0); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + { + guid: profileID, + }, + { + guid: deletedID, + deleted: true, + }, + ]); + + strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0); + strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedID), 0); + + // Validate incoming records with unknown fields get stored + let localRecord = await profileStorage.addresses.get(profileID); + equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]); + + // The sync applied new records - ensure our tracker knew it came from + // sync and didn't bump the score. + equal(engine._tracker.score, 0); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_incoming_existing() { + let { profileStorage, server, engine } = await setup(); + try { + let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1); + let guid2 = await profileStorage.addresses.add(TEST_PROFILE_2); + + // an initial sync so we don't think they are locally modified. + await engine.setLastSync(0); + await engine.sync(); + + // now server records that modify the existing items. + let modifiedEntry1 = Object.assign({}, TEST_PROFILE_1, { + version: 1, + "given-name": "NewName", + }); + + let lastSync = await engine.getLastSync(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid1, + encryptPayload({ + id: guid1, + entry: modifiedEntry1, + }), + lastSync + 10 + ) + ); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid2, + encryptPayload({ + id: guid2, + deleted: true, + }), + lastSync + 10 + ) + ); + + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + Object.assign({}, modifiedEntry1, { guid: guid1 }), + { guid: guid2, deleted: true }, + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_tombstones() { + let { profileStorage, server, collection, engine } = await setup(); + try { + let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + Assert.equal(collection.count(), 1); + let payload = collection.payloads()[0]; + equal(payload.id, existingGUID); + equal(payload.deleted, undefined); + + profileStorage.addresses.remove(existingGUID); + await engine.sync(); + + // should still exist, but now be a tombstone. + Assert.equal(collection.count(), 1); + payload = collection.payloads()[0]; + equal(payload.id, existingGUID); + equal(payload.deleted, true); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_both_deleted() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + // Delete synced record locally. + profileStorage.addresses.remove(guid); + + // Delete same record remotely. + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + lastSync + 10 + ); + + await engine.sync(); + + ok( + !(await await profileStorage.addresses.get(guid)), + "Should not return record for locally deleted item" + ); + + let localRecords = await profileStorage.addresses.getAll({ + includeDeleted: true, + }); + equal(localRecords.length, 1, "Only tombstone should exist locally"); + + equal(collection.count(), 1, "Only tombstone should exist on server"); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_nonexistent_tombstone() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = profileStorage.addresses._generateGUID(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + getDateForSync() + ); + + await engine.setLastSync(0); + await engine.sync(); + + ok( + !(await profileStorage.addresses.get(guid)), + "Should not return record for uknown deleted item" + ); + let localTombstone = ( + await profileStorage.addresses.getAll({ + includeDeleted: true, + }) + ).find(record => record.guid == guid); + ok(localTombstone, "Should store tombstone for unknown item"); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_incoming_deleted() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + // Delete the record remotely. + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + lastSync + 10 + ); + + await engine.sync(); + + ok( + !(await profileStorage.addresses.get(guid)), + "Should delete unmodified item locally" + ); + + let localTombstone = ( + await profileStorage.addresses.getAll({ + includeDeleted: true, + }) + ).find(record => record.guid == guid); + ok(localTombstone, "Should keep local tombstone for remotely deleted item"); + strictEqual( + getSyncChangeCounter(profileStorage.addresses, guid), + 0, + "Local tombstone should be marked as syncing" + ); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_incoming_restored() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // Upload the record to the server. + await engine.setLastSync(0); + await engine.sync(); + + // Removing a synced record should write a tombstone. + profileStorage.addresses.remove(guid); + + // Modify the deleted record remotely. + let collection = server.user("foo").collection("addresses"); + let serverPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + serverPayload.entry["street-address"] = "I moved!"; + let lastSync = await engine.getLastSync(); + collection.insert(guid, encryptPayload(serverPayload), lastSync + 10); + + // Sync again. + await engine.sync(); + + // We should replace our tombstone with the server's version. + let localRecord = await profileStorage.addresses.get(guid); + ok( + objectMatches(localRecord, { + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved!", + }) + ); + + let maybeNewServerPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + deepEqual( + maybeNewServerPayload, + serverPayload, + "Should not change record on server" + ); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_outgoing_restored() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // Upload the record to the server. + await engine.setLastSync(0); + await engine.sync(); + + // Modify the local record. + let localCopy = Object.assign({}, TEST_PROFILE_1); + localCopy["street-address"] = "I moved!"; + await profileStorage.addresses.update(guid, localCopy); + + // Replace the record with a tombstone on the server. + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + lastSync + 10 + ); + + // Sync again. + await engine.sync(); + + // We should resurrect the record on the server. + let serverPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + ok(!serverPayload.deleted, "Should resurrect record on server"); + ok( + objectMatches(serverPayload.entry, { + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved!", + // resurrection also beings back any unknown fields we had + "unknown-1": "some unknown data from another client", + }) + ); + + let localRecord = await profileStorage.addresses.get(guid); + ok(localRecord, "Modified record should not be deleted locally"); + } finally { + await cleanup(server); + } +}); + +// Unlike most sync engines, we want "both modified" to inspect the records, +// and if materially different, create a duplicate. +add_task(async function test_reconcile_both_modified_identical() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // and an identical record on the server. + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid, + encryptPayload({ + id: guid, + entry: TEST_PROFILE_1, + }), + getDateForSync() + ) + ); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [{ guid }]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_incoming_dupes() { + let { profileStorage, server, engine } = await setup(); + try { + // Create a profile locally, then sync to upload the new profile to the + // server. + let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + // Create another profile locally, but don't sync it yet. + await profileStorage.addresses.add(TEST_PROFILE_2); + + // Now create two records on the server with the same contents as our local + // profiles, but different GUIDs. + let lastSync = await engine.getLastSync(); + let guid1_dupe = Utils.makeGUID(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid1_dupe, + encryptPayload({ + id: guid1_dupe, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + lastSync + 10 + ) + ); + let guid2_dupe = Utils.makeGUID(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid2_dupe, + encryptPayload({ + id: guid2_dupe, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_2 + ), + }), + lastSync + 10 + ) + ); + + // Sync again. We should download `guid1_dupe` and `guid2_dupe`, then + // reconcile changes. + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + // We uploaded `guid1` during the first sync. Even though its contents + // are the same as `guid1_dupe`, we keep both. + Object.assign({}, TEST_PROFILE_1, { guid: guid1 }), + Object.assign({}, TEST_PROFILE_1, { guid: guid1_dupe }), + // However, we didn't upload `guid2` before downloading `guid2_dupe`, so + // we *should* dedupe `guid2` to `guid2_dupe`. + Object.assign({}, TEST_PROFILE_2, { guid: guid2_dupe }), + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_dedupe_identical_unsynced() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // and an identical record on the server but different GUID. + let remoteGuid = Utils.makeGUID(); + notEqual(localGuid, remoteGuid); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + remoteGuid, + encryptPayload({ + id: remoteGuid, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + getDateForSync() + ) + ); + + await engine.setLastSync(0); + await engine.sync(); + + // Should have 1 item locally with GUID changed to the remote one. + // There's no tombstone as the original was unsynced. + await expectLocalProfiles(profileStorage, [ + { + guid: remoteGuid, + }, + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_dedupe_identical_synced() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // sync it - it will no longer be a candidate for de-duping. + await engine.setLastSync(0); + await engine.sync(); + + // and an identical record on the server but different GUID. + let lastSync = await engine.getLastSync(); + let remoteGuid = Utils.makeGUID(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + remoteGuid, + encryptPayload({ + id: remoteGuid, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + lastSync + 10 + ) + ); + + await engine.sync(); + + // Should have 2 items locally, since the first was synced. + await expectLocalProfiles(profileStorage, [ + { guid: localGuid }, + { guid: remoteGuid }, + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_dedupe_multiple_candidates() { + let { profileStorage, server, engine } = await setup(); + try { + // It's possible to have duplicate local profiles, with the same fields but + // different GUIDs. After a node reassignment, or after disconnecting and + // reconnecting to Sync, we might dedupe a local record A to a remote record + // B, if we see B before we download and apply A. Since A and B are dupes, + // that's OK. We'll write a tombstone for A when we dedupe A to B, and + // overwrite that tombstone when we see A. + + let localRecord = { + "given-name": "Mark", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + tel: "+12345678910", + }; + let serverRecord = Object.assign( + { + version: 1, + }, + localRecord + ); + + // We don't pass `sourceSync` so that the records are marked as NEW. + let aGuid = await profileStorage.addresses.add(localRecord); + let bGuid = await profileStorage.addresses.add(localRecord); + + // Insert B before A. + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + bGuid, + encryptPayload({ + id: bGuid, + entry: serverRecord, + }), + getDateForSync() + ) + ); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + aGuid, + encryptPayload({ + id: aGuid, + entry: serverRecord, + }), + getDateForSync() + ) + ); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + { + guid: aGuid, + "given-name": "Mark", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + tel: "+12345678910", + }, + { + guid: bGuid, + "given-name": "Mark", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + tel: "+12345678910", + }, + ]); + // Make sure these are both syncing. + strictEqual( + getSyncChangeCounter(profileStorage.addresses, aGuid), + 0, + "A should be marked as syncing" + ); + strictEqual( + getSyncChangeCounter(profileStorage.addresses, bGuid), + 0, + "B should be marked as syncing" + ); + } finally { + await cleanup(server); + } +}); + +// Unlike most sync engines, we want "both modified" to inspect the records, +// and if materially different, create a duplicate. +add_task(async function test_reconcile_both_modified_conflict() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // Upload the record to the server. + await engine.setLastSync(0); + await engine.sync(); + + strictEqual( + getSyncChangeCounter(profileStorage.addresses, guid), + 0, + "Original record should be marked as syncing" + ); + + // Change the same field locally and on the server. + let localCopy = Object.assign({}, TEST_PROFILE_1); + localCopy["street-address"] = "I moved!"; + await profileStorage.addresses.update(guid, localCopy); + + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + let serverPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + serverPayload.entry["street-address"] = "I moved, too!"; + collection.insert(guid, encryptPayload(serverPayload), lastSync + 10); + + // Sync again. + await engine.sync(); + + // Since we wait to pull changes until we're ready to upload, both records + // should now exist on the server; we don't need a follow-up sync. + let serverPayloads = collection.payloads(); + equal(serverPayloads.length, 2, "Both records should exist on server"); + + let forkedPayload = serverPayloads.find(payload => payload.id != guid); + ok(forkedPayload, "Forked record should exist on server"); + + await expectLocalProfiles(profileStorage, [ + { + guid, + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved, too!", + }, + { + guid: forkedPayload.id, + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved!", + }, + ]); + + let changeCounter = getSyncChangeCounter( + profileStorage.addresses, + forkedPayload.id + ); + strictEqual(changeCounter, 0, "Forked record should be marked as syncing"); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_wipe() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + await expectLocalProfiles(profileStorage, [{ guid }]); + + let promiseObserved = promiseOneObserver("formautofill-storage-changed"); + + await engine._wipeClient(); + + let { subject, data } = await promiseObserved; + Assert.equal( + subject.wrappedJSObject.sourceSync, + true, + "it should be noted this came from sync" + ); + Assert.equal( + subject.wrappedJSObject.collectionName, + "addresses", + "got the correct collection" + ); + Assert.equal(data, "removeAll", "a removeAll should be noted"); + + await expectLocalProfiles(profileStorage, []); + } finally { + await cleanup(server); + } +}); + +// Other clients might have data that we aren't able to process/understand yet +// We should keep that data and ensure when we sync we don't lose that data +add_task(async function test_full_roundtrip_unknown_data() { + let { profileStorage, server, engine } = await setup(); + try { + let profileID = Utils.makeGUID(); + + info("Incoming records with unknown fields are properly stored"); + // Insert a record onto the server + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + profileID, + encryptPayload({ + id: profileID, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + getDateForSync() + ) + ); + + // The tracker should start with no score. + equal(engine._tracker.score, 0); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + { + guid: profileID, + }, + ]); + + strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0); + + // The sync applied new records - ensure our tracker knew it came from + // sync and didn't bump the score. + equal(engine._tracker.score, 0); + + // Validate incoming records with unknown fields are correctly stored + let localRecord = await profileStorage.addresses.get(profileID); + equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]); + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == "update" + ); + + // Validate we can update the records locally and not drop any unknown fields + info("Unknown fields are sent back up to the server"); + + // Modify the local copy + let localCopy = Object.assign({}, TEST_PROFILE_1); + localCopy["street-address"] = "I moved!"; + await profileStorage.addresses.update(profileID, localCopy); + await onChanged; + await profileStorage._saveImmediately(); + + let updatedCopy = await profileStorage.addresses.get(profileID); + equal(updatedCopy["street-address"], "I moved!"); + + // Sync our changes to the server + await engine.setLastSync(0); + await engine.sync(); + + let collection = server.user("foo").collection("addresses"); + + Assert.ok(collection.wbo(profileID)); + let serverPayload = JSON.parse( + JSON.parse(collection.payload(profileID)).ciphertext + ); + + // The server has the updated field as well as any unknown fields + equal( + serverPayload.entry["unknown-1"], + "some unknown data from another client" + ); + equal(serverPayload.entry["street-address"], "I moved!"); + } finally { + await cleanup(server); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_sync_deprecate_credit_card_v4.js b/browser/extensions/formautofill/test/unit/test_sync_deprecate_credit_card_v4.js new file mode 100644 index 0000000000..2362796b0c --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_sync_deprecate_credit_card_v4.js @@ -0,0 +1,248 @@ +/** + * Tests sync functionality. + */ + +/* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */ +/* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */ +/* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */ +/* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */ + +"use strict"; + +const { Service } = ChromeUtils.importESModule( + "resource://services-sync/service.sys.mjs" +); + +const { CreditCardsEngine } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillSync.sys.mjs" +); + +Services.prefs.setCharPref("extensions.formautofill.loglevel", "Trace"); +initTestLogging("Trace"); + +const TEST_STORE_FILE_NAME = "test-profile.json"; + +const TEST_CREDIT_CARD_1 = { + guid: "86d961c7717a", + "cc-name": "John Doe", + "cc-number": "4111111111111111", + "cc-exp-month": 4, + "cc-exp-year": new Date().getFullYear(), +}; + +const TEST_CREDIT_CARD_2 = { + guid: "cf57a7ac3539", + "cc-name": "Timothy Berners-Lee", + "cc-number": "4929001587121045", + "cc-exp-month": 12, + "cc-exp-year": new Date().getFullYear() + 10, +}; + +function expectProfiles(profiles, expected) { + expected.sort((a, b) => a.guid.localeCompare(b.guid)); + profiles.sort((a, b) => a.guid.localeCompare(b.guid)); + try { + deepEqual( + profiles.map(p => p.guid), + expected.map(p => p.guid) + ); + for (let i = 0; i < expected.length; i++) { + let thisExpected = expected[i]; + let thisGot = profiles[i]; + // always check "deleted". + equal(thisExpected.deleted, thisGot.deleted); + ok(objectMatches(thisGot, thisExpected)); + } + } catch (ex) { + info("Comparing expected profiles:"); + info(JSON.stringify(expected, undefined, 2)); + info("against actual profiles:"); + info(JSON.stringify(profiles, undefined, 2)); + throw ex; + } +} + +async function expectServerProfiles(collection, expected) { + const profiles = collection + .payloads() + .map(payload => Object.assign({ guid: payload.id }, payload.entry)); + expectProfiles(profiles, expected); +} + +async function expectLocalProfiles(profileStorage, expected) { + const profiles = await profileStorage.creditCards.getAll({ + rawData: true, + includeDeleted: true, + }); + expectProfiles(profiles, expected); +} + +async function setup() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + // should always start with no profiles. + Assert.equal( + (await profileStorage.creditCards.getAll({ includeDeleted: true })).length, + 0 + ); + + Services.prefs.setCharPref( + "services.sync.log.logger.engine.CreditCards", + "Trace" + ); + let engine = new CreditCardsEngine(Service); + await engine.initialize(); + // Avoid accidental automatic sync due to our own changes + Service.scheduler.syncThreshold = 10000000; + let syncID = await engine.resetLocalSyncID(); + let server = serverForUsers( + { foo: "password" }, + { + meta: { + global: { + engines: { creditcards: { version: engine.version, syncID } }, + }, + }, + creditcards: {}, + } + ); + + Service.engineManager._engines.creditcards = engine; + engine.enabled = true; + engine._store._storage = profileStorage.creditCards; + + generateNewKeys(Service.collectionKeys); + + await SyncTestingInfrastructure(server); + + let collection = server.user("foo").collection("creditcards"); + + return { profileStorage, server, collection, engine }; +} + +async function cleanup(server) { + let promiseStartOver = promiseOneObserver("weave:service:start-over:finish"); + await Service.startOver(); + await promiseStartOver; + await promiseStopServer(server); +} + +function getTestRecords(profileStorage, version) { + return [ + Object.assign({ version }, TEST_CREDIT_CARD_1), + Object.assign({ version }, TEST_CREDIT_CARD_2), + ]; +} + +function setupServerRecords(server, records) { + for (const record of records) { + server.insertWBO( + "foo", + "creditcards", + new ServerWBO( + record.guid, + encryptPayload({ + id: record.guid, + entry: Object.assign({}, record), + }), + getDateForSync() + ) + ); + } +} + +/** + * We want to setup old records and run init() to migrate records. + * However, We don't have an easy way to setup an older version record with + * init() function now. + * So as a workaround, we simulate the behavior by directly setting data and then + * run migration. + */ +async function setupLocalProfilesAndRunMigration(profileStorage, records) { + for (const record of records) { + profileStorage._store.data.creditCards.push(Object.assign({}, record)); + } + await Promise.all( + profileStorage.creditCards._data.map(async (record, index) => + profileStorage.creditCards._migrateRecord(record, index) + ) + ); +} + +// local v3, server v4 +add_task(async function test_local_v3_server_v4() { + let { collection, profileStorage, server, engine } = await setup(); + + const V3_RECORDS = getTestRecords(profileStorage, 3); + const V4_RECORDS = getTestRecords(profileStorage, 4); + + await setupLocalProfilesAndRunMigration(profileStorage, V3_RECORDS); + setupServerRecords(server, V4_RECORDS); + + await engine.setLastSync(0); + await engine.sync(); + + await expectServerProfiles(collection, V3_RECORDS); + await expectLocalProfiles(profileStorage, V3_RECORDS); + + await cleanup(server); +}); + +// local v4, server empty +add_task(async function test_local_v4_server_empty() { + let { collection, profileStorage, server, engine } = await setup(); + const V3_RECORDS = getTestRecords(profileStorage, 3); + const V4_RECORDS = getTestRecords(profileStorage, 4); + + await setupLocalProfilesAndRunMigration(profileStorage, V4_RECORDS); + + await engine.setLastSync(0); + await engine.sync(); + + await expectServerProfiles(collection, V3_RECORDS); + await expectLocalProfiles(profileStorage, V3_RECORDS); + + await cleanup(server); +}); + +// local v4, server v3 +add_task(async function test_local_v4_server_v3() { + let { collection, profileStorage, server, engine } = await setup(); + const V3_RECORDS = getTestRecords(profileStorage, 3); + const V4_RECORDS = getTestRecords(profileStorage, 4); + + await setupLocalProfilesAndRunMigration(profileStorage, V4_RECORDS); + setupServerRecords(server, V3_RECORDS); + + // local should be v3 before syncing. + await expectLocalProfiles(profileStorage, V3_RECORDS); + + await engine.setLastSync(0); + await engine.sync(); + + await expectServerProfiles(collection, V3_RECORDS); + await expectLocalProfiles(profileStorage, V3_RECORDS); + + await cleanup(server); +}); + +// local v4, server v4 +add_task(async function test_local_v4_server_v4() { + let { collection, profileStorage, server, engine } = await setup(); + const V3_RECORDS = getTestRecords(profileStorage, 3); + const V4_RECORDS = getTestRecords(profileStorage, 4); + + await setupLocalProfilesAndRunMigration(profileStorage, V4_RECORDS); + setupServerRecords(server, V4_RECORDS); + + // local should be v3 before syncing and then we ignore + // incoming v4 from server + await expectLocalProfiles(profileStorage, V3_RECORDS); + + await engine.setLastSync(0); + await engine.sync(); + + await expectServerProfiles(collection, V3_RECORDS); + await expectLocalProfiles(profileStorage, V3_RECORDS); + + await cleanup(server); +}); diff --git a/browser/extensions/formautofill/test/unit/test_toOneLineAddress.js b/browser/extensions/formautofill/test/unit/test_toOneLineAddress.js new file mode 100644 index 0000000000..ee04c8d1d5 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_toOneLineAddress.js @@ -0,0 +1,64 @@ +"use strict"; + +var FormAutofillUtils; +add_setup(async () => { + ({ FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" + )); +}); + +add_task(async function test_getCategoriesFromFieldNames() { + const TEST_CASES = [ + { + strings: ["A", "B", "C", "D"], + expectedValue: "A B C D", + }, + { + strings: ["A", "B", "", "D"], + expectedValue: "A B D", + }, + { + strings: ["", "B", "", "D"], + expectedValue: "B D", + }, + { + strings: [null, "B", " ", "D"], + expectedValue: "B D", + }, + { + strings: "A B C", + expectedValue: "A B C", + }, + { + strings: "A\nB\n\n\nC", + expectedValue: "A B C", + }, + { + strings: "A B \nC", + expectedValue: "A B C", + }, + { + strings: "A-B-C", + expectedValue: "A B C", + delimiter: "-", + }, + { + strings: "A B\n \nC", + expectedValue: "A B C", + }, + { + strings: null, + expectedValue: "", + }, + ]; + + for (let tc of TEST_CASES) { + let result; + if (tc.delimiter) { + result = FormAutofillUtils.toOneLineAddress(tc.strings, tc.delimiter); + } else { + result = FormAutofillUtils.toOneLineAddress(tc.strings); + } + Assert.equal(result, tc.expectedValue); + } +}); diff --git a/browser/extensions/formautofill/test/unit/test_transformFields.js b/browser/extensions/formautofill/test/unit/test_transformFields.js new file mode 100644 index 0000000000..47ba396e06 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_transformFields.js @@ -0,0 +1,972 @@ +/** + * Tests the transform algorithm in profileStorage. + */ + +"use strict"; + +let FormAutofillStorage; +add_setup(async () => { + ({ FormAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + )); +}); + +const TEST_STORE_FILE_NAME = "test-profile.json"; + +const ADDRESS_COMPUTE_TESTCASES = [ + // Name + { + description: "Has split names", + address: { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + }, + expectedResult: { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + name: "Timothy John Berners-Lee", + }, + }, + { + description: "Has split CJK names", + address: { + "given-name": "德明", + "family-name": "孫", + }, + expectedResult: { + "given-name": "德明", + "family-name": "孫", + name: "孫德明", + }, + }, + + // Address + { + description: '"street-address" with single line', + address: { + "street-address": "single line", + }, + expectedResult: { + "street-address": "single line", + "address-line1": "single line", + }, + }, + { + description: '"street-address" with multiple lines', + address: { + "street-address": "line1\nline2\nline3", + }, + expectedResult: { + "street-address": "line1\nline2\nline3", + "address-line1": "line1", + "address-line2": "line2", + "address-line3": "line3", + }, + }, + { + description: '"street-address" with multiple lines but line2 is omitted', + address: { + "street-address": "line1\n\nline3", + }, + expectedResult: { + "street-address": "line1\n\nline3", + "address-line1": "line1", + "address-line2": undefined, + "address-line3": "line3", + }, + }, + { + description: '"street-address" with 4 lines', + address: { + "street-address": "line1\nline2\nline3\nline4", + }, + expectedResult: { + "street-address": "line1\nline2\nline3\nline4", + "address-line1": "line1", + "address-line2": "line2", + "address-line3": "line3 line4", + }, + }, + { + description: '"street-address" with blank lines', + address: { + "street-address": "line1\n \nline3\n \nline5", + }, + expectedResult: { + "street-address": "line1\n \nline3\n \nline5", + "address-line1": "line1", + "address-line2": undefined, + "address-line3": "line3 line5", + }, + }, + + // Country + { + description: 'Has "country"', + address: { + country: "US", + }, + expectedResult: { + country: "US", + "country-name": "United States", + }, + }, + + // Tel + { + description: '"tel" with US country code', + address: { + tel: "+16172535702", + }, + expectedResult: { + tel: "+16172535702", + "tel-country-code": "+1", + "tel-national": "6172535702", + "tel-area-code": "617", + "tel-local": "2535702", + "tel-local-prefix": "253", + "tel-local-suffix": "5702", + }, + }, + { + description: '"tel" with TW country code (the components won\'t be parsed)', + address: { + tel: "+886212345678", + }, + expectedResult: { + tel: "+886212345678", + "tel-country-code": "+886", + "tel-national": "0212345678", + "tel-area-code": undefined, + "tel-local": undefined, + "tel-local-prefix": undefined, + "tel-local-suffix": undefined, + }, + }, + { + description: '"tel" without country code so use "US" as default resion', + address: { + tel: "6172535702", + }, + expectedResult: { + tel: "+16172535702", + "tel-country-code": "+1", + "tel-national": "6172535702", + "tel-area-code": "617", + "tel-local": "2535702", + "tel-local-prefix": "253", + "tel-local-suffix": "5702", + }, + }, + { + description: '"tel" without country code but "country" is "TW"', + address: { + tel: "0212345678", + country: "TW", + }, + expectedResult: { + tel: "+886212345678", + "tel-country-code": "+886", + "tel-national": "0212345678", + "tel-area-code": undefined, + "tel-local": undefined, + "tel-local-prefix": undefined, + "tel-local-suffix": undefined, + }, + }, + { + description: '"tel" can\'t be parsed so leave it as-is', + address: { + tel: "12345", + }, + expectedResult: { + tel: "12345", + "tel-country-code": undefined, + "tel-national": "12345", + "tel-area-code": undefined, + "tel-local": undefined, + "tel-local-prefix": undefined, + "tel-local-suffix": undefined, + }, + }, +]; + +const ADDRESS_NORMALIZE_TESTCASES = [ + // Name + { + description: 'Has "name", and the split names are omitted', + address: { + name: "Timothy John Berners-Lee", + }, + expectedResult: { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + }, + }, + { + description: 'Has both "name" and split names', + address: { + name: "John Doe", + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + }, + expectedResult: { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + }, + }, + { + description: 'Has "name", and some of split names are omitted', + address: { + name: "John Doe", + "given-name": "Timothy", + }, + expectedResult: { + "given-name": "Timothy", + "family-name": "Doe", + }, + }, + + // Address + { + description: 'Has "address-line1~3" and "street-address" is omitted', + address: { + "address-line1": "line1", + "address-line2": "line2", + "address-line3": "line3", + }, + expectedResult: { + "street-address": "line1\nline2\nline3", + }, + }, + { + description: 'Has both "address-line1~3" and "street-address"', + address: { + "street-address": "street address", + "address-line1": "line1", + "address-line2": "line2", + "address-line3": "line3", + }, + expectedResult: { + "street-address": "street address", + }, + }, + { + description: 'Has "address-line2~3" and single-line "street-address"', + address: { + "street-address": "street address", + "address-line2": "line2", + "address-line3": "line3", + }, + expectedResult: { + "street-address": "street address\nline2\nline3", + }, + }, + { + description: 'Has "address-line2~3" and multiple-line "street-address"', + address: { + "street-address": "street address\nstreet address line 2", + "address-line2": "line2", + "address-line3": "line3", + }, + expectedResult: { + "street-address": "street address\nstreet address line 2", + }, + }, + { + description: 'Has only "address-line1~2"', + address: { + "address-line1": "line1", + "address-line2": "line2", + }, + expectedResult: { + "street-address": "line1\nline2", + }, + }, + { + description: 'Has only "address-line1"', + address: { + "address-line1": "line1", + }, + expectedResult: { + "street-address": "line1", + }, + }, + { + description: 'Has only "address-line2~3"', + address: { + "address-line2": "line2", + "address-line3": "line3", + }, + expectedResult: { + "street-address": "\nline2\nline3", + }, + }, + { + description: 'Has only "address-line2"', + address: { + "address-line2": "line2", + }, + expectedResult: { + "street-address": "\nline2", + }, + }, + + // Country + { + description: 'Has "country" in lowercase', + address: { + country: "us", + }, + expectedResult: { + country: "US", + }, + }, + { + description: 'Has unknown "country"', + address: { + "given-name": "John", // Make sure it won't be an empty record. + country: "AA", + }, + expectedResult: { + country: undefined, + }, + }, + { + description: 'Has "country-name"', + address: { + "country-name": "united states", + }, + expectedResult: { + country: "US", + "country-name": "United States", + }, + }, + { + description: 'Has alternative "country-name"', + address: { + "country-name": "america", + }, + expectedResult: { + country: "US", + "country-name": "United States", + }, + }, + { + description: 'Has "country-name" as a substring', + address: { + "country-name": "test america test", + }, + expectedResult: { + country: "US", + "country-name": "United States", + }, + }, + { + description: 'Has "country-name" as part of a word', + address: { + "given-name": "John", // Make sure it won't be an empty record. + "country-name": "TRUST", + }, + expectedResult: { + country: undefined, + "country-name": undefined, + }, + }, + { + description: 'Has unknown "country-name"', + address: { + "given-name": "John", // Make sure it won't be an empty record. + "country-name": "unknown country name", + }, + expectedResult: { + country: undefined, + "country-name": undefined, + }, + }, + { + description: 'Has "country" and unknown "country-name"', + address: { + country: "us", + "country-name": "unknown country name", + }, + expectedResult: { + country: "US", + "country-name": "United States", + }, + }, + { + description: 'Has "country-name" and unknown "country"', + address: { + "given-name": "John", // Make sure it won't be an empty record. + country: "AA", + "country-name": "united states", + }, + expectedResult: { + country: undefined, + "country-name": undefined, + }, + }, + { + description: 'Has unsupported "country"', + address: { + "given-name": "John", // Make sure it won't be an empty record. + country: "XX", + }, + expectedResult: { + country: undefined, + "country-name": undefined, + }, + }, + + // Tel + { + description: 'Has "tel" with country code', + address: { + tel: "+16172535702", + }, + expectedResult: { + tel: "+16172535702", + }, + }, + { + description: 'Has "tel" without country code but "country" is set', + address: { + tel: "0212345678", + country: "TW", + }, + expectedResult: { + tel: "+886212345678", + }, + }, + { + description: + 'Has "tel" without country code and "country" so use "US" as default region', + address: { + tel: "6172535702", + }, + expectedResult: { + tel: "+16172535702", + }, + }, + { + description: '"tel" can\'t be parsed so leave it as-is', + address: { + tel: "12345", + }, + expectedResult: { + tel: "12345", + }, + }, + { + description: 'Has a valid tel-local format "tel"', + address: { + tel: "1234567", + }, + expectedResult: { + tel: "1234567", + }, + }, + { + description: 'Has "tel-national" and "tel-country-code"', + address: { + "tel-national": "0212345678", + "tel-country-code": "+886", + }, + expectedResult: { + tel: "+886212345678", + }, + }, + { + description: 'Has "tel-national" and "country"', + address: { + "tel-national": "0212345678", + country: "TW", + }, + expectedResult: { + tel: "+886212345678", + }, + }, + { + description: 'Has "tel-national", "tel-country-code" and "country"', + address: { + "tel-national": "0212345678", + "tel-country-code": "+886", + country: "US", + }, + expectedResult: { + tel: "+886212345678", + }, + }, + { + description: 'Has "tel-area-code" and "tel-local"', + address: { + "tel-area-code": "617", + "tel-local": "2535702", + }, + expectedResult: { + tel: "+16172535702", + }, + }, + { + description: + 'Has "tel-area-code", "tel-local-prefix" and "tel-local-suffix"', + address: { + "tel-area-code": "617", + "tel-local-prefix": "253", + "tel-local-suffix": "5702", + }, + expectedResult: { + tel: "+16172535702", + }, + }, +]; + +const CREDIT_CARD_COMPUTE_TESTCASES = [ + // Name + { + description: 'Has "cc-name"', + creditCard: { + "cc-name": "Timothy John Berners-Lee", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-name": "Timothy John Berners-Lee", + "cc-number": "************1045", + "cc-given-name": "Timothy", + "cc-additional-name": "John", + "cc-family-name": "Berners-Lee", + }, + }, + + // Card Number + { + description: "Number should be encrypted and masked", + creditCard: { + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-number": "************1045", + }, + }, + + // Expiration Date + { + description: 'Has "cc-exp-year" and "cc-exp-month"', + creditCard: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-exp": "2022-12", + "cc-number": "************1045", + }, + }, + { + description: 'Has only "cc-exp-month"', + creditCard: { + "cc-exp-month": 12, + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp": undefined, + "cc-number": "************1045", + }, + }, + { + description: 'Has only "cc-exp-year"', + creditCard: { + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-year": 2022, + "cc-exp": undefined, + "cc-number": "************1045", + }, + }, +]; + +const CREDIT_CARD_NORMALIZE_TESTCASES = [ + // Name + { + description: 'Has both "cc-name" and the split name fields', + creditCard: { + "cc-name": "Timothy John Berners-Lee", + "cc-given-name": "John", + "cc-family-name": "Doe", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-name": "Timothy John Berners-Lee", + "cc-number": "4929001587121045", + }, + }, + { + description: "Has only the split name fields", + creditCard: { + "cc-given-name": "John", + "cc-family-name": "Doe", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-name": "John Doe", + "cc-number": "4929001587121045", + }, + }, + + // Card Number + { + description: "Regular number", + creditCard: { + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-number": "4929001587121045", + }, + }, + { + description: "Number with spaces", + creditCard: { + "cc-number": "4111 1111 1111 1111", + }, + expectedResult: { + "cc-number": "4111111111111111", + }, + }, + { + description: "Number with hyphens", + creditCard: { + "cc-number": "4111-1111-1111-1111", + }, + expectedResult: { + "cc-number": "4111111111111111", + }, + }, + + // Expiration Date + { + description: 'Has "cc-exp" formatted "yyyy-mm"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "2022-12", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "yyyy/mm"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "2022/12", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "yyyy-m"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "2022-3", + }, + expectedResult: { + "cc-exp-month": 3, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "yyyy/m"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "2022/3", + }, + expectedResult: { + "cc-exp-month": 3, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "mm-yyyy"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "12-2022", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "mm/yyyy"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "12/2022", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "m-yyyy"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "3-2022", + }, + expectedResult: { + "cc-exp-month": 3, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "m/yyyy"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "3/2022", + }, + expectedResult: { + "cc-exp-month": 3, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "mm-yy"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "12-22", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "mm/yy"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "12/22", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "yy-mm"', + creditCard: { + "cc-number": "4929001587121045", + "cc-exp": "22-12", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "yy/mm"', + creditCard: { + "cc-exp": "22/12", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "mmyy"', + creditCard: { + "cc-exp": "1222", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" formatted "yymm"', + creditCard: { + "cc-exp": "2212", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has "cc-exp" with spaces', + creditCard: { + "cc-exp": " 2033-11 ", + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 11, + "cc-exp-year": 2033, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has invalid "cc-exp"', + creditCard: { + "cc-number": "4111111111111111", // Make sure it won't be an empty record. + "cc-exp": "99-9999", + }, + expectedResult: { + "cc-exp-month": undefined, + "cc-exp-year": undefined, + }, + }, + { + description: 'Has both "cc-exp-*" and "cc-exp"', + creditCard: { + "cc-exp": "2022-12", + "cc-exp-month": 3, + "cc-exp-year": 2030, + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 3, + "cc-exp-year": 2030, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has only "cc-exp-year" and "cc-exp"', + creditCard: { + "cc-exp": "2022-12", + "cc-exp-year": 2030, + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, + { + description: 'Has only "cc-exp-month" and "cc-exp"', + creditCard: { + "cc-exp": "2022-12", + "cc-exp-month": 3, + "cc-number": "4929001587121045", + }, + expectedResult: { + "cc-exp-month": 12, + "cc-exp-year": 2022, + "cc-number": "4929001587121045", + }, + }, +]; + +let do_check_record_matches = (expectedRecord, record) => { + for (let key in expectedRecord) { + Assert.equal(expectedRecord[key], record[key]); + } +}; + +add_task(async function test_computeAddressFields() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + for (let testcase of ADDRESS_COMPUTE_TESTCASES) { + info("Verify testcase: " + testcase.description); + + let guid = await profileStorage.addresses.add(testcase.address); + let address = await profileStorage.addresses.get(guid); + do_check_record_matches(testcase.expectedResult, address); + + profileStorage.addresses.remove(guid); + } + + await profileStorage._finalize(); +}); + +add_task(async function test_normalizeAddressFields() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + for (let testcase of ADDRESS_NORMALIZE_TESTCASES) { + info("Verify testcase: " + testcase.description); + + let guid = await profileStorage.addresses.add(testcase.address); + let address = await profileStorage.addresses.get(guid); + do_check_record_matches(testcase.expectedResult, address); + + profileStorage.addresses.remove(guid); + } + + await profileStorage._finalize(); +}); + +add_task(async function test_computeCreditCardFields() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + for (let testcase of CREDIT_CARD_COMPUTE_TESTCASES) { + info("Verify testcase: " + testcase.description); + + let guid = await profileStorage.creditCards.add(testcase.creditCard); + let creditCard = await profileStorage.creditCards.get(guid); + do_check_record_matches(testcase.expectedResult, creditCard); + + profileStorage.creditCards.remove(guid); + } + + await profileStorage._finalize(); +}); + +add_task(async function test_normalizeCreditCardFields() { + let path = getTempFile(TEST_STORE_FILE_NAME).path; + + let profileStorage = new FormAutofillStorage(path); + await profileStorage.initialize(); + + for (let testcase of CREDIT_CARD_NORMALIZE_TESTCASES) { + info("Verify testcase: " + testcase.description); + + let guid = await profileStorage.creditCards.add(testcase.creditCard); + let creditCard = await profileStorage.creditCards.get(guid, { + rawData: true, + }); + do_check_record_matches(testcase.expectedResult, creditCard); + + profileStorage.creditCards.remove(guid); + } + + await profileStorage._finalize(); +}); diff --git a/browser/extensions/formautofill/test/unit/xpcshell.ini b/browser/extensions/formautofill/test/unit/xpcshell.ini new file mode 100644 index 0000000000..bb0a71729c --- /dev/null +++ b/browser/extensions/formautofill/test/unit/xpcshell.ini @@ -0,0 +1,100 @@ +[DEFAULT] +skip-if = + (os == "linux") && ccov # bug 1821945 + toolkit == 'android' # bug 1730213 +firefox-appdir = browser +head = head.js +support-files = + ../fixtures/** +prefs = + extensions.formautofill.heuristics.visibilityCheckThreshold=0 + +[test_activeStatus.js] +[test_addressComponent_city.js] +head = head_addressComponent.js +[test_addressComponent_country.js] +head = head_addressComponent.js +[test_addressComponent_email.js] +head = head_addressComponent.js +[test_addressComponent_name.js] +head = head_addressComponent.js +[test_addressComponent_organization.js] +head = head_addressComponent.js +[test_addressComponent_postal_code.js] +head = head_addressComponent.js +[test_addressComponent_state.js] +head = head_addressComponent.js +[test_addressComponent_street_address.js] +head = head_addressComponent.js +[test_addressComponent_tel.js] +head = head_addressComponent.js +[test_addressDataLoader.js] +[test_addressRecords.js] +skip-if = + apple_silicon # bug 1729554 +[test_autofillFormFields.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_clearPopulatedForm.js] +[test_collectFormFields.js] +[test_createRecords.js] +[test_creditCardRecords.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_extractLabelStrings.js] +[test_findLabelElements.js] +[test_getAdaptedProfiles.js] +[test_getAdaptedProfiles_locales.js] +[test_getCategoriesFromFieldNames.js] +[test_getCreditCardLogo.js] +[test_getFormInputDetails.js] +[test_getInfo.js] +[test_getRecords.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_isAddressAutofillAvailable.js] +[test_isCJKName.js] +[test_isCreditCardAutofillAvailable.js] +[test_isCreditCardOrAddressFieldType.js] +[test_known_strings.js] +[test_markAsAutofillField.js] +[test_migrateRecords.js] +skip-if = tsan # Times out, bug 1612707 +[test_nameUtils.js] +[test_onFormSubmitted.js] +skip-if = tsan # Times out, bug 1612707 +[test_parseStreetAddress.js] +[test_parseAddressFormat.js] +[test_previewFormFields.js] +[test_profileAutocompleteResult.js] +[test_phoneNumber.js] +[test_reconcile.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_savedFieldNames.js] +[test_toOneLineAddress.js] +[test_storage_tombstones.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_storage_remove.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_storage_syncfields.js] +[test_transformFields.js] +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 +[test_sync.js] +head = head.js ../../../../../services/sync/tests/unit/head_appinfo.js ../../../../../services/common/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_http_server.js +skip-if = tsan # Times out, bug 1612707 +[test_sync_deprecate_credit_card_v4.js] +head = head.js ../../../../../services/sync/tests/unit/head_appinfo.js ../../../../../services/common/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_http_server.js +skip-if = + tsan # Times out, bug 1612707 + apple_silicon # bug 1729554 -- cgit v1.2.3