summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /browser/extensions/formautofill
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/extensions/formautofill')
-rw-r--r--browser/extensions/formautofill/.eslintrc.js82
-rw-r--r--browser/extensions/formautofill/api.js222
-rw-r--r--browser/extensions/formautofill/background.js15
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.js644
-rw-r--r--browser/extensions/formautofill/content/customElements.js410
-rw-r--r--browser/extensions/formautofill/content/editAddress.xhtml134
-rw-r--r--browser/extensions/formautofill/content/editCreditCard.xhtml122
-rw-r--r--browser/extensions/formautofill/content/editDialog.js239
-rw-r--r--browser/extensions/formautofill/content/formautofill.css54
-rw-r--r--browser/extensions/formautofill/content/formfill-anchor.svg8
-rw-r--r--browser/extensions/formautofill/content/icon-address-save.svg6
-rw-r--r--browser/extensions/formautofill/content/icon-address-update.svg6
-rw-r--r--browser/extensions/formautofill/content/icon-credit-card-generic.svg8
-rw-r--r--browser/extensions/formautofill/content/icon-credit-card.svg8
-rw-r--r--browser/extensions/formautofill/content/manageAddresses.xhtml54
-rw-r--r--browser/extensions/formautofill/content/manageCreditCards.xhtml55
-rw-r--r--browser/extensions/formautofill/content/manageDialog.css125
-rw-r--r--browser/extensions/formautofill/content/manageDialog.js464
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-amex.pngbin0 -> 1306 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.pngbin0 -> 2311 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.pngbin0 -> 1240 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.pngbin0 -> 3111 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-diners.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-discover.pngbin0 -> 1117 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.pngbin0 -> 2471 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-mir.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-visa.svg1
-rw-r--r--browser/extensions/formautofill/docs/heuristics.rst36
-rw-r--r--browser/extensions/formautofill/docs/index.rst30
-rw-r--r--browser/extensions/formautofill/jar.mn7
-rw-r--r--browser/extensions/formautofill/locales/en-US/formautofill.properties127
-rw-r--r--browser/extensions/formautofill/locales/jar.mn8
-rw-r--r--browser/extensions/formautofill/locales/moz.build7
-rw-r--r--browser/extensions/formautofill/manifest.json26
-rw-r--r--browser/extensions/formautofill/moz.build58
-rw-r--r--browser/extensions/formautofill/schema.json1
-rw-r--r--browser/extensions/formautofill/skin/linux/autocomplete-item.css10
-rw-r--r--browser/extensions/formautofill/skin/linux/editDialog.css8
-rw-r--r--browser/extensions/formautofill/skin/osx/autocomplete-item.css18
-rw-r--r--browser/extensions/formautofill/skin/osx/editDialog.css5
-rw-r--r--browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css188
-rw-r--r--browser/extensions/formautofill/skin/shared/editAddress.css134
-rw-r--r--browser/extensions/formautofill/skin/shared/editCreditCard.css53
-rw-r--r--browser/extensions/formautofill/skin/shared/editDialog-shared.css110
-rw-r--r--browser/extensions/formautofill/skin/windows/autocomplete-item.css25
-rw-r--r--browser/extensions/formautofill/skin/windows/editDialog.css12
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser.ini13
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js240
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js691
-rw-r--r--browser/extensions/formautofill/test/browser/address/head_address.js1
-rw-r--r--browser/extensions/formautofill/test/browser/browser.ini35
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js137
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js66
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js58
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autofill_address_select.js64
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js95
-rw-r--r--browser/extensions/formautofill/test/browser/browser_check_installed.js12
-rw-r--r--browser/extensions/formautofill/test/browser/browser_dropdown_layout.js53
-rw-r--r--browser/extensions/formautofill/test/browser/browser_editAddressDialog.js951
-rw-r--r--browser/extensions/formautofill/test/browser/browser_fathom_cc.js204
-rw-r--r--browser/extensions/formautofill/test/browser/browser_first_time_use_doorhanger.js142
-rw-r--r--browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js105
-rw-r--r--browser/extensions/formautofill/test/browser/browser_privacyPreferences.js439
-rw-r--r--browser/extensions/formautofill/test/browser/browser_remoteiframe.js127
-rw-r--r--browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js37
-rw-r--r--browser/extensions/formautofill/test/browser/browser_update_doorhanger.js189
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser.ini51
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js123
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js170
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js311
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js198
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js103
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js238
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js117
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js57
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js37
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js165
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js77
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js104
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js109
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js872
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js422
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js145
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js290
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/head_cc.js1
-rw-r--r--browser/extensions/formautofill/test/browser/empty.html8
-rwxr-xr-xbrowser/extensions/formautofill/test/browser/fathom/test-setup.sh39
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg3
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.pngbin0 -> 4968 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gifbin0 -> 37 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg16
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg14
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg11
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.binbin0 -> 9594 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg8
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg6
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg6
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg8
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2bin0 -> 15480 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2bin0 -> 15784 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2bin0 -> 15908 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/sample.html20
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/browser.ini12
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js56
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml7
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html6
-rw-r--r--browser/extensions/formautofill/test/browser/head.js1094
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser.ini17
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js74
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js102
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_basic.js69
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js56
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js32
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js27
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_ignore_invisible_fields.js115
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js118
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_parseAddressFields.js138
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js79
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js318
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser.ini22
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js82
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js71
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js170
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js25
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js25
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js24
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js77
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js28
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js31
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js88
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js109
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js83
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js96
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js81
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js78
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js93
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html26
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_basic.html52
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html29
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_cc_exp_field.html28
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_iframe.html12
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_iframe.html13
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_off_on_form.html52
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_off_on_inputs.html77
-rw-r--r--browser/extensions/formautofill/test/fixtures/autocomplete_simple_basic.html19
-rw-r--r--browser/extensions/formautofill/test/fixtures/heuristics_cc_exp.html73
-rw-r--r--browser/extensions/formautofill/test/fixtures/heuristics_de_fields.html122
-rw-r--r--browser/extensions/formautofill/test/fixtures/heuristics_fr_fields.html34
-rw-r--r--browser/extensions/formautofill/test/fixtures/multiple_section.html84
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_Payment.html283
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/BestBuy/Checkout_ShippingAddress.html326
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/BestBuy/SignIn.html21
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_BillingPaymentInfo.html469
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_Logon.html118
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/CDW/Checkout_ShippingInfo.html376
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/CostCo/Payment.html892
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/CostCo/ShippingAddress.html527
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/CostCo/SignIn.html374
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/DirectAsda/Payment.html90
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Ebay/Checkout_Payment_FR.html135
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/GlobalDirectAsda/Payment.html154
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/Checkout_ShippingPayment.html381
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/HomeDepot/SignIn.html83
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Lufthansa/Checkout_Payment.html23
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Lush/index.html421
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_Payment.html474
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Macys/Checkout_ShippingAddress.html439
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Macys/SignIn.html208
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html1074
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/NewEgg/Login.html156
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/NewEgg/ShippingInfo.html270
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/Payment.html672
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/ShippingAddress.html347
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/OfficeDepot/SignIn.html44
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/QVC/PaymentMethod.html527
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/QVC/SignIn.html80
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/QVC/YourInformation.html522
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/README4
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Sears/PaymentOptions.html566
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Sears/ShippingAddress.html447
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic.html117
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Staples/Basic_ac_on.html117
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling.html99
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Staples/PaymentBilling_ac_on.html98
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Walmart/Checkout.html243
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Walmart/Payment.html235
-rw-r--r--browser/extensions/formautofill/test/fixtures/third_party/Walmart/Shipping.html234
-rw-r--r--browser/extensions/formautofill/test/fixtures/without_autocomplete_address_basic.html26
-rw-r--r--browser/extensions/formautofill/test/fixtures/without_autocomplete_creditcard_basic.html53
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini26
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html251
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html205
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html211
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html96
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html174
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html110
-rw-r--r--browser/extensions/formautofill/test/mochitest/formautofill_common.js478
-rw-r--r--browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js304
-rw-r--r--browser/extensions/formautofill/test/mochitest/mochitest.ini23
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html102
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html116
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_autofocus_form.html69
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html220
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_form_changes.html128
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html121
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html273
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_multiple_forms.html67
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_on_address_submission.html121
-rw-r--r--browser/extensions/formautofill/test/unit/head.js357
-rw-r--r--browser/extensions/formautofill/test/unit/head_addressComponent.js69
-rw-r--r--browser/extensions/formautofill/test/unit/test_activeStatus.js176
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_city.js27
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_country.js47
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_email.js74
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_name.js101
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_organization.js55
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_postal_code.js57
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_state.js32
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_street_address.js56
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_tel.js76
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressDataLoader.js102
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressRecords.js858
-rw-r--r--browser/extensions/formautofill/test/unit/test_autofillFormFields.js1078
-rw-r--r--browser/extensions/formautofill/test/unit/test_clearPopulatedForm.js116
-rw-r--r--browser/extensions/formautofill/test/unit/test_collectFormFields.js638
-rw-r--r--browser/extensions/formautofill/test/unit/test_createRecords.js525
-rw-r--r--browser/extensions/formautofill/test/unit/test_creditCardRecords.js926
-rw-r--r--browser/extensions/formautofill/test/unit/test_extractLabelStrings.js77
-rw-r--r--browser/extensions/formautofill/test/unit/test_findLabelElements.js100
-rw-r--r--browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js1300
-rw-r--r--browser/extensions/formautofill/test/unit/test_getAdaptedProfiles_locales.js272
-rw-r--r--browser/extensions/formautofill/test/unit/test_getCategoriesFromFieldNames.js95
-rw-r--r--browser/extensions/formautofill/test/unit/test_getCreditCardLogo.js25
-rw-r--r--browser/extensions/formautofill/test/unit/test_getFormInputDetails.js204
-rw-r--r--browser/extensions/formautofill/test/unit/test_getInfo.js363
-rw-r--r--browser/extensions/formautofill/test/unit/test_getRecords.js258
-rw-r--r--browser/extensions/formautofill/test/unit/test_isAddressAutofillAvailable.js74
-rw-r--r--browser/extensions/formautofill/test/unit/test_isCJKName.js80
-rw-r--r--browser/extensions/formautofill/test/unit/test_isCreditCardAutofillAvailable.js84
-rw-r--r--browser/extensions/formautofill/test/unit/test_isCreditCardOrAddressFieldType.js103
-rw-r--r--browser/extensions/formautofill/test/unit/test_known_strings.js148
-rw-r--r--browser/extensions/formautofill/test/unit/test_markAsAutofillField.js201
-rw-r--r--browser/extensions/formautofill/test/unit/test_migrateRecords.js382
-rw-r--r--browser/extensions/formautofill/test/unit/test_nameUtils.js289
-rw-r--r--browser/extensions/formautofill/test/unit/test_onFormSubmitted.js805
-rw-r--r--browser/extensions/formautofill/test/unit/test_parseAddressFormat.js66
-rw-r--r--browser/extensions/formautofill/test/unit/test_parseStreetAddress.js74
-rw-r--r--browser/extensions/formautofill/test/unit/test_phoneNumber.js399
-rw-r--r--browser/extensions/formautofill/test/unit/test_previewFormFields.js199
-rw-r--r--browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js450
-rw-r--r--browser/extensions/formautofill/test/unit/test_reconcile.js1173
-rw-r--r--browser/extensions/formautofill/test/unit/test_savedFieldNames.js106
-rw-r--r--browser/extensions/formautofill/test/unit/test_storage_remove.js88
-rw-r--r--browser/extensions/formautofill/test/unit/test_storage_syncfields.js498
-rw-r--r--browser/extensions/formautofill/test/unit/test_storage_tombstones.js190
-rw-r--r--browser/extensions/formautofill/test/unit/test_sync.js1017
-rw-r--r--browser/extensions/formautofill/test/unit/test_sync_deprecate_credit_card_v4.js248
-rw-r--r--browser/extensions/formautofill/test/unit/test_toOneLineAddress.js64
-rw-r--r--browser/extensions/formautofill/test/unit/test_transformFields.js972
-rw-r--r--browser/extensions/formautofill/test/unit/xpcshell.ini100
267 files changed, 45735 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/.eslintrc.js b/browser/extensions/formautofill/.eslintrc.js
new file mode 100644
index 0000000000..f290c1b3c1
--- /dev/null
+++ b/browser/extensions/formautofill/.eslintrc.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ rules: {
+ // Rules from the mozilla plugin
+ "mozilla/balanced-listeners": "error",
+ "mozilla/no-aArgs": "error",
+ "mozilla/var-only-at-top-level": "error",
+
+ // No expressions where a statement is expected
+ "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": [
+ "error",
+ {
+ args: "none",
+ vars: "all",
+ },
+ ],
+
+ // No using variables before defined
+ "no-use-before-define": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": "error",
+
+ // Warn about cyclomatic complexity in functions.
+ complexity: ["error", { max: 26 }],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 4],
+
+ // Disallow using the console API, except for error statments.
+ "no-console": ["error", { allow: ["error"] }],
+
+ // Disallow fallthrough of case statements, except if there is a comment.
+ "no-fallthrough": "error",
+
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": "error",
+
+ // Disallow usage of __proto__ property.
+ "no-proto": "error",
+
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": "error",
+
+ // Require use of the second argument for parseInt().
+ radix: "error",
+
+ // Require "use strict" to be defined globally in the script.
+ strict: ["error", "global"],
+
+ // Disallow Yoda conditions (where literal value comes first).
+ yoda: "error",
+
+ // Disallow function or variable declarations in nested blocks
+ "no-inner-declarations": "error",
+ },
+
+ overrides: [
+ {
+ files: "**/head.js",
+ rules: {
+ "no-unused-vars": [
+ "error",
+ {
+ args: "none",
+ vars: "local",
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/browser/extensions/formautofill/api.js b/browser/extensions/formautofill/api.js
new file mode 100644
index 0000000000..000c393e8e
--- /dev/null
+++ b/browser/extensions/formautofill/api.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* globals ExtensionAPI, Services, XPCOMUtils */
+
+const CACHED_STYLESHEETS = new WeakMap();
+
+ChromeUtils.defineESModuleGetters(this, {
+ FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
+ FormAutofillParent: "resource://autofill/FormAutofillParent.sys.mjs",
+ FormAutofillStatus: "resource://autofill/FormAutofillParent.sys.mjs",
+ AutoCompleteParent: "resource://gre/actors/AutoCompleteParent.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler"
+);
+
+const RESOURCE_HOST = "formautofill";
+
+function insertStyleSheet(domWindow, url) {
+ let doc = domWindow.document;
+ let styleSheetAttr = `href="${url}" type="text/css"`;
+ let styleSheet = doc.createProcessingInstruction(
+ "xml-stylesheet",
+ styleSheetAttr
+ );
+
+ doc.insertBefore(styleSheet, doc.documentElement);
+
+ if (CACHED_STYLESHEETS.has(domWindow)) {
+ CACHED_STYLESHEETS.get(domWindow).push(styleSheet);
+ } else {
+ CACHED_STYLESHEETS.set(domWindow, [styleSheet]);
+ }
+}
+
+function ensureCssLoaded(domWindow) {
+ if (CACHED_STYLESHEETS.has(domWindow)) {
+ // This window already has autofill stylesheets.
+ return;
+ }
+
+ insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css");
+ insertStyleSheet(
+ domWindow,
+ "chrome://formautofill/content/skin/autocomplete-item-shared.css"
+ );
+ insertStyleSheet(
+ domWindow,
+ "chrome://formautofill/content/skin/autocomplete-item.css"
+ );
+}
+
+this.formautofill = class extends ExtensionAPI {
+ /**
+ * Adjusts and checks form autofill preferences during startup.
+ *
+ * @param {boolean} addressAutofillAvailable
+ * @param {boolean} creditCardAutofillAvailable
+ */
+ adjustAndCheckFormAutofillPrefs(
+ addressAutofillAvailable,
+ creditCardAutofillAvailable
+ ) {
+ // Reset the sync prefs in case the features were previously available
+ // but aren't now.
+ if (!creditCardAutofillAvailable) {
+ Services.prefs.clearUserPref(
+ "services.sync.engine.creditcards.available"
+ );
+ }
+ if (!addressAutofillAvailable) {
+ Services.prefs.clearUserPref("services.sync.engine.addresses.available");
+ }
+
+ if (!addressAutofillAvailable && !creditCardAutofillAvailable) {
+ Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
+ Services.telemetry.scalarSet("formautofill.availability", false);
+ return;
+ }
+
+ // This pref is used for web contents to detect the autocomplete feature.
+ // When it's true, "element.autocomplete" will return tokens we currently
+ // support -- otherwise it'll return an empty string.
+ Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true);
+ Services.telemetry.scalarSet("formautofill.availability", true);
+
+ // These "*.available" prefs determines whether the "addresses"/"creditcards" sync engine is
+ // available (ie, whether it is shown in any UI etc) - it *does not* determine
+ // whether the engine is actually enabled or not.
+ if (FormAutofill.isAutofillAddressesAvailable) {
+ Services.prefs.setBoolPref(
+ "services.sync.engine.addresses.available",
+ true
+ );
+ } else {
+ Services.prefs.clearUserPref("services.sync.engine.addresses.available");
+ }
+ if (FormAutofill.isAutofillCreditCardsAvailable) {
+ Services.prefs.setBoolPref(
+ "services.sync.engine.creditcards.available",
+ true
+ );
+ } else {
+ Services.prefs.clearUserPref(
+ "services.sync.engine.creditcards.available"
+ );
+ }
+ }
+ onStartup() {
+ // We have to do this before actually determining if we're enabled, since
+ // there are scripts inside of the core browser code that depend on the
+ // FormAutofill JSMs being registered.
+ let uri = Services.io.newURI("chrome/res/", null, this.extension.rootURI);
+ resProto.setSubstitution(RESOURCE_HOST, uri);
+
+ let aomStartup = Cc[
+ "@mozilla.org/addons/addon-manager-startup;1"
+ ].getService(Ci.amIAddonManagerStartup);
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ this.extension.rootURI
+ );
+ this.chromeHandle = aomStartup.registerChrome(manifestURI, [
+ ["content", "formautofill", "chrome/content/"],
+ ]);
+
+ // Until we move to fluent (bug 1446164), we're stuck with
+ // chrome.manifest for handling localization since its what the
+ // build system can handle for localized repacks.
+ if (this.extension.rootURI instanceof Ci.nsIJARURI) {
+ this.autofillManifest = this.extension.rootURI.JARFile.QueryInterface(
+ Ci.nsIFileURL
+ ).file;
+ } else if (this.extension.rootURI instanceof Ci.nsIFileURL) {
+ this.autofillManifest = this.extension.rootURI.file;
+ }
+
+ if (this.autofillManifest) {
+ Components.manager.addBootstrappedManifestLocation(this.autofillManifest);
+ } else {
+ console.error(
+ "Cannot find formautofill chrome.manifest for registring translated strings"
+ );
+ }
+ let addressAutofillAvailable = FormAutofill.isAutofillAddressesAvailable;
+ let creditCardAutofillAvailable =
+ FormAutofill.isAutofillCreditCardsAvailable;
+ this.adjustAndCheckFormAutofillPrefs(
+ addressAutofillAvailable,
+ creditCardAutofillAvailable
+ );
+ if (!creditCardAutofillAvailable && !addressAutofillAvailable) {
+ return;
+ }
+ // Listen for the autocomplete popup message
+ // or the form submitted message (which may trigger a
+ // doorhanger) to lazily append our stylesheets related
+ // to the autocomplete feature.
+ AutoCompleteParent.addPopupStateListener(ensureCssLoaded);
+ FormAutofillParent.addMessageObserver(this);
+ this.onFormSubmitted = (data, window) => ensureCssLoaded(window);
+
+ FormAutofillStatus.init();
+
+ ChromeUtils.registerWindowActor("FormAutofill", {
+ parent: {
+ esModuleURI: "resource://autofill/FormAutofillParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs",
+ events: {
+ focusin: {},
+ DOMFormBeforeSubmit: {},
+ },
+ },
+ allFrames: true,
+ });
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return;
+ }
+
+ resProto.setSubstitution(RESOURCE_HOST, null);
+
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+
+ if (this.autofillManifest) {
+ Components.manager.removeBootstrappedManifestLocation(
+ this.autofillManifest
+ );
+ }
+
+ ChromeUtils.unregisterWindowActor("FormAutofill");
+
+ AutoCompleteParent.removePopupStateListener(ensureCssLoaded);
+ FormAutofillParent.removeMessageObserver(this);
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ let cachedStyleSheets = CACHED_STYLESHEETS.get(win);
+
+ if (!cachedStyleSheets) {
+ continue;
+ }
+
+ while (cachedStyleSheets.length !== 0) {
+ cachedStyleSheets.pop().remove();
+ }
+ }
+ }
+};
diff --git a/browser/extensions/formautofill/background.js b/browser/extensions/formautofill/background.js
new file mode 100644
index 0000000000..fe6265415f
--- /dev/null
+++ b/browser/extensions/formautofill/background.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env webextensions */
+
+"use strict";
+
+browser.runtime.onUpdateAvailable.addListener(details => {
+ // By listening to but ignoring this event, any updates will
+ // be delayed until the next browser restart.
+ // Note that if we ever wanted to change this, we should make
+ // sure we manually invalidate the startup cache using the
+ // startupcache-invalidate notification.
+});
diff --git a/browser/extensions/formautofill/content/autofillEditForms.js b/browser/extensions/formautofill/content/autofillEditForms.js
new file mode 100644
index 0000000000..3ed64a098a
--- /dev/null
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -0,0 +1,644 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported EditAddress, EditCreditCard */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+class EditAutofillForm {
+ constructor(elements) {
+ this._elements = elements;
+ }
+
+ /**
+ * Fill the form with a record object.
+ *
+ * @param {object} [record = {}]
+ */
+ loadRecord(record = {}) {
+ for (let field of this._elements.form.elements) {
+ let value = record[field.id];
+ value = typeof value == "undefined" ? "" : value;
+
+ if (record.guid) {
+ field.value = value;
+ } else if (field.localName == "select") {
+ this.setDefaultSelectedOptionByValue(field, value);
+ } else {
+ // Use .defaultValue instead of .value to avoid setting the `dirty` flag
+ // which triggers form validation UI.
+ field.defaultValue = value;
+ }
+ }
+ if (!record.guid) {
+ // Reset the dirty value flag and validity state.
+ this._elements.form.reset();
+ } else {
+ for (let field of this._elements.form.elements) {
+ this.updatePopulatedState(field);
+ this.updateCustomValidity(field);
+ }
+ }
+ }
+
+ setDefaultSelectedOptionByValue(select, value) {
+ for (let option of select.options) {
+ option.defaultSelected = option.value == value;
+ }
+ }
+
+ /**
+ * Get a record from the form suitable for a save/update in storage.
+ *
+ * @returns {object}
+ */
+ buildFormObject() {
+ let initialObject = {};
+ if (this.hasMailingAddressFields) {
+ // Start with an empty string for each mailing-address field so that any
+ // fields hidden for the current country are blanked in the return value.
+ initialObject = {
+ "street-address": "",
+ "address-level3": "",
+ "address-level2": "",
+ "address-level1": "",
+ "postal-code": "",
+ };
+ }
+
+ return Array.from(this._elements.form.elements).reduce((obj, input) => {
+ if (!input.disabled) {
+ obj[input.id] = input.value;
+ }
+ return obj;
+ }, initialObject);
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "change": {
+ this.handleChange(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle change events
+ *
+ * @param {DOMEvent} event
+ */
+ handleChange(event) {
+ this.updatePopulatedState(event.target);
+ }
+
+ /**
+ * Handle input events
+ *
+ * @param {DOMEvent} event
+ */
+ handleInput(event) {}
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ this._elements.form.addEventListener("input", this);
+ }
+
+ /**
+ * Set the field-populated attribute if the field has a value.
+ *
+ * @param {DOMElement} field The field that will be checked for a value.
+ */
+ updatePopulatedState(field) {
+ let span = field.parentNode.querySelector(".label-text");
+ if (!span) {
+ return;
+ }
+ span.toggleAttribute("field-populated", !!field.value.trim());
+ }
+
+ /**
+ * Run custom validity routines specific to the field and type of form.
+ *
+ * @param {DOMElement} field The field that will be validated.
+ */
+ updateCustomValidity(field) {}
+}
+
+class EditAddress extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record
+ * @param {object} config
+ * @param {boolean} [config.noValidate=undefined] Whether to validate the form
+ */
+ constructor(elements, record, config) {
+ super(elements);
+
+ Object.assign(this, config);
+ let { form } = this._elements;
+ Object.assign(this._elements, {
+ addressLevel3Label: form.querySelector(
+ "#address-level3-container > .label-text"
+ ),
+ addressLevel2Label: form.querySelector(
+ "#address-level2-container > .label-text"
+ ),
+ addressLevel1Label: form.querySelector(
+ "#address-level1-container > .label-text"
+ ),
+ postalCodeLabel: form.querySelector(
+ "#postal-code-container > .label-text"
+ ),
+ country: form.querySelector("#country"),
+ });
+
+ this.populateCountries();
+ // Need to populate the countries before trying to set the initial country.
+ // Also need to use this._record so it has the default country selected.
+ this.loadRecord(record);
+ this.attachEventListeners();
+
+ form.noValidate = !!config.noValidate;
+ }
+
+ loadRecord(record) {
+ this._record = record;
+ if (!record) {
+ record = {
+ country: FormAutofill.DEFAULT_REGION,
+ };
+ }
+
+ let { addressLevel1Options } = FormAutofillUtils.getFormFormat(
+ record.country
+ );
+ this.populateAddressLevel1(addressLevel1Options, record.country);
+
+ super.loadRecord(record);
+ this.loadAddressLevel1(record["address-level1"], record.country);
+ this.formatForm(record.country);
+ }
+
+ get hasMailingAddressFields() {
+ let { addressFields } = this._elements.form.dataset;
+ return (
+ !addressFields ||
+ addressFields.trim().split(/\s+/).includes("mailing-address")
+ );
+ }
+
+ /**
+ * `mailing-address` is a special attribute token to indicate mailing fields + country.
+ *
+ * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
+ * @param {string} addressFields - white-space-separated string of requested address fields to show
+ * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
+ */
+ static computeVisibleFields(mailingFieldsOrder, addressFields) {
+ if (addressFields) {
+ let requestedFieldClasses = addressFields.trim().split(/\s+/);
+ let fieldClasses = [];
+ if (requestedFieldClasses.includes("mailing-address")) {
+ fieldClasses = fieldClasses.concat(mailingFieldsOrder);
+ // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
+ requestedFieldClasses.splice(
+ requestedFieldClasses.indexOf("mailing-address"),
+ 1,
+ "country"
+ );
+ }
+
+ for (let fieldClassName of requestedFieldClasses) {
+ fieldClasses.push({
+ fieldId: fieldClassName,
+ newLine: fieldClassName == "name",
+ });
+ }
+ return fieldClasses;
+ }
+
+ // This is the default which is shown in the management interface and includes all fields.
+ return mailingFieldsOrder.concat([
+ {
+ fieldId: "country",
+ },
+ {
+ fieldId: "tel",
+ },
+ {
+ fieldId: "email",
+ newLine: true,
+ },
+ ]);
+ }
+
+ /**
+ * Format the form based on country. The address-level1 and postal-code labels
+ * should be specific to the given country.
+ *
+ * @param {string} country
+ */
+ formatForm(country) {
+ const {
+ addressLevel3L10nId,
+ addressLevel2L10nId,
+ addressLevel1L10nId,
+ addressLevel1Options,
+ postalCodeL10nId,
+ fieldsOrder: mailingFieldsOrder,
+ postalCodePattern,
+ countryRequiredFields,
+ } = FormAutofillUtils.getFormFormat(country);
+
+ document.l10n.setAttributes(
+ this._elements.addressLevel3Label,
+ addressLevel3L10nId
+ );
+ document.l10n.setAttributes(
+ this._elements.addressLevel2Label,
+ addressLevel2L10nId
+ );
+ document.l10n.setAttributes(
+ this._elements.addressLevel1Label,
+ addressLevel1L10nId
+ );
+ document.l10n.setAttributes(
+ this._elements.postalCodeLabel,
+ postalCodeL10nId
+ );
+ let addressFields = this._elements.form.dataset.addressFields;
+ let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
+ let fieldClasses = EditAddress.computeVisibleFields(
+ mailingFieldsOrder,
+ addressFields
+ );
+ let requiredFields = new Set(countryRequiredFields);
+ if (extraRequiredFields) {
+ for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
+ requiredFields.add(extraRequiredField);
+ }
+ }
+ this.arrangeFields(fieldClasses, requiredFields);
+ this.updatePostalCodeValidation(postalCodePattern);
+ this.populateAddressLevel1(addressLevel1Options, country);
+ }
+
+ /**
+ * Update address field visibility and order based on libaddressinput data.
+ *
+ * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
+ * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
+ */
+ arrangeFields(fieldsOrder, requiredFields) {
+ /**
+ * @see FormAutofillStorage.VALID_ADDRESS_FIELDS
+ */
+ let fields = [
+ // `name` is a wrapper for the 3 name fields.
+ "name",
+ "organization",
+ "street-address",
+ "address-level3",
+ "address-level2",
+ "address-level1",
+ "postal-code",
+ "country",
+ "tel",
+ "email",
+ ];
+ let inputs = [];
+ for (let i = 0; i < fieldsOrder.length; i++) {
+ let { fieldId, newLine } = fieldsOrder[i];
+
+ let container = this._elements.form.querySelector(
+ `#${fieldId}-container`
+ );
+ let containerInputs = [
+ ...container.querySelectorAll("input, textarea, select"),
+ ];
+ containerInputs.forEach(function (input) {
+ input.disabled = false;
+ // libaddressinput doesn't list 'country' or 'name' as required.
+ // The additional-name field should never get marked as required.
+ input.required =
+ (fieldId == "country" ||
+ fieldId == "name" ||
+ requiredFields.has(fieldId)) &&
+ input.id != "additional-name";
+ });
+ inputs.push(...containerInputs);
+ container.style.display = "flex";
+ container.style.order = i;
+ container.style.pageBreakAfter = newLine ? "always" : "auto";
+ // Remove the field from the list of fields
+ fields.splice(fields.indexOf(fieldId), 1);
+ }
+ for (let i = 0; i < inputs.length; i++) {
+ // Assign tabIndex starting from 1
+ inputs[i].tabIndex = i + 1;
+ }
+ // Hide the remaining fields
+ for (let field of fields) {
+ let container = this._elements.form.querySelector(`#${field}-container`);
+ container.style.display = "none";
+ for (let input of [
+ ...container.querySelectorAll("input, textarea, select"),
+ ]) {
+ input.disabled = true;
+ }
+ }
+ }
+
+ updatePostalCodeValidation(postalCodePattern) {
+ let postalCodeInput = this._elements.form.querySelector("#postal-code");
+ if (postalCodePattern && postalCodeInput.style.display != "none") {
+ postalCodeInput.setAttribute("pattern", postalCodePattern);
+ } else {
+ postalCodeInput.removeAttribute("pattern");
+ }
+ }
+
+ /**
+ * Set the address-level1 value on the form field (input or select, whichever is present).
+ *
+ * @param {string} addressLevel1Value Value of the address-level1 from the autofill record
+ * @param {string} country The corresponding country
+ */
+ loadAddressLevel1(addressLevel1Value, country) {
+ let field = this._elements.form.querySelector("#address-level1");
+
+ if (field.localName == "input") {
+ field.value = addressLevel1Value || "";
+ return;
+ }
+
+ let matchedSelectOption = FormAutofillUtils.findAddressSelectOption(
+ field,
+ {
+ country,
+ "address-level1": addressLevel1Value,
+ },
+ "address-level1"
+ );
+ if (matchedSelectOption && !matchedSelectOption.selected) {
+ field.value = matchedSelectOption.value;
+ field.dispatchEvent(new Event("input", { bubbles: true }));
+ field.dispatchEvent(new Event("change", { bubbles: true }));
+ } else if (addressLevel1Value) {
+ // If the option wasn't found, insert an option at the beginning of
+ // the select that matches the stored value.
+ field.insertBefore(
+ new Option(addressLevel1Value, addressLevel1Value, true, true),
+ field.firstChild
+ );
+ }
+ }
+
+ /**
+ * Replace the text input for address-level1 with a select dropdown if
+ * a fixed set of names exists. Otherwise show a text input.
+ *
+ * @param {Map?} options Map of options with regionCode -> name mappings
+ * @param {string} country The corresponding country
+ */
+ populateAddressLevel1(options, country) {
+ let field = this._elements.form.querySelector("#address-level1");
+
+ if (field.dataset.country == country) {
+ return;
+ }
+
+ if (!options) {
+ if (field.localName == "input") {
+ return;
+ }
+
+ let input = document.createElement("input");
+ input.setAttribute("type", "text");
+ input.id = "address-level1";
+ input.required = field.required;
+ input.disabled = field.disabled;
+ input.tabIndex = field.tabIndex;
+ field.replaceWith(input);
+ return;
+ }
+
+ if (field.localName == "input") {
+ let select = document.createElement("select");
+ select.id = "address-level1";
+ select.required = field.required;
+ select.disabled = field.disabled;
+ select.tabIndex = field.tabIndex;
+ field.replaceWith(select);
+ field = select;
+ }
+
+ field.textContent = "";
+ field.dataset.country = country;
+ let fragment = document.createDocumentFragment();
+ fragment.appendChild(new Option(undefined, undefined, true, true));
+ for (let [regionCode, regionName] of options) {
+ let option = new Option(regionName, regionCode);
+ fragment.appendChild(option);
+ }
+ field.appendChild(fragment);
+ }
+
+ populateCountries() {
+ let fragment = document.createDocumentFragment();
+ // Sort countries by their visible names.
+ let countries = [...FormAutofill.countries.entries()].sort((e1, e2) =>
+ e1[1].localeCompare(e2[1])
+ );
+ for (let [country] of countries) {
+ const countryName = Services.intl.getRegionDisplayNames(undefined, [
+ country.toLowerCase(),
+ ]);
+ const option = new Option(countryName, country);
+ fragment.appendChild(option);
+ }
+ this._elements.country.appendChild(fragment);
+ }
+
+ handleChange(event) {
+ if (event.target == this._elements.country) {
+ this.formatForm(event.target.value);
+ }
+ super.handleChange(event);
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+}
+
+class EditCreditCard extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record with a decrypted cc-number
+ * @param {object} addresses in an object with guid keys for the billing address picker.
+ */
+ constructor(elements, record, addresses) {
+ super(elements);
+
+ this._addresses = addresses;
+ Object.assign(this._elements, {
+ ccNumber: this._elements.form.querySelector("#cc-number"),
+ invalidCardNumberStringElement: this._elements.form.querySelector(
+ "#invalidCardNumberString"
+ ),
+ month: this._elements.form.querySelector("#cc-exp-month"),
+ year: this._elements.form.querySelector("#cc-exp-year"),
+ billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
+ billingAddressRow:
+ this._elements.form.querySelector(".billingAddressRow"),
+ });
+
+ this.attachEventListeners();
+ this.loadRecord(record, addresses);
+ }
+
+ loadRecord(record, addresses, preserveFieldValues) {
+ // _record must be updated before generateYears and generateBillingAddressOptions are called.
+ this._record = record;
+ this._addresses = addresses;
+ this.generateBillingAddressOptions(preserveFieldValues);
+ if (!preserveFieldValues) {
+ // Re-generating the months will reset the selected option.
+ this.generateMonths();
+ // Re-generating the years will reset the selected option.
+ this.generateYears();
+ super.loadRecord(record);
+ }
+ }
+
+ generateMonths() {
+ const count = 12;
+
+ // Clear the list
+ this._elements.month.textContent = "";
+
+ // Empty month option
+ this._elements.month.appendChild(new Option());
+
+ // Populate month list. Format: "month number - month name"
+ let dateFormat = new Intl.DateTimeFormat(navigator.language, {
+ month: "long",
+ }).format;
+ for (let i = 0; i < count; i++) {
+ let monthNumber = (i + 1).toString();
+ let monthName = dateFormat(new Date(1970, i));
+ let option = new Option();
+ option.value = monthNumber;
+ // XXX: Bug 1446164 - Localize this string.
+ option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
+ this._elements.month.appendChild(option);
+ }
+ }
+
+ generateYears() {
+ const count = 11;
+ const currentYear = new Date().getFullYear();
+ const ccExpYear = this._record && this._record["cc-exp-year"];
+
+ // Clear the list
+ this._elements.year.textContent = "";
+
+ // Provide an empty year option
+ this._elements.year.appendChild(new Option());
+
+ if (ccExpYear && ccExpYear < currentYear) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+
+ for (let i = 0; i < count; i++) {
+ let year = currentYear + i;
+ let option = new Option(year);
+ this._elements.year.appendChild(option);
+ }
+
+ if (ccExpYear && ccExpYear > currentYear + count) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+ }
+
+ generateBillingAddressOptions(preserveFieldValues) {
+ let billingAddressGUID;
+ if (preserveFieldValues && this._elements.billingAddress.value) {
+ billingAddressGUID = this._elements.billingAddress.value;
+ } else if (this._record) {
+ billingAddressGUID = this._record.billingAddressGUID;
+ }
+
+ this._elements.billingAddress.textContent = "";
+
+ this._elements.billingAddress.appendChild(new Option("", ""));
+
+ let hasAddresses = false;
+ for (let [guid, address] of Object.entries(this._addresses)) {
+ hasAddresses = true;
+ let selected = guid == billingAddressGUID;
+ let option = new Option(
+ FormAutofillUtils.getAddressLabel(address),
+ guid,
+ selected,
+ selected
+ );
+ this._elements.billingAddress.appendChild(option);
+ }
+
+ this._elements.billingAddressRow.hidden = !hasAddresses;
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+
+ handleInput(event) {
+ // Clear the error message if cc-number is valid
+ if (
+ event.target == this._elements.ccNumber &&
+ FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
+ ) {
+ this._elements.ccNumber.setCustomValidity("");
+ }
+ super.handleInput(event);
+ }
+
+ updateCustomValidity(field) {
+ super.updateCustomValidity(field);
+
+ // Mark the cc-number field as invalid if the number is empty or invalid.
+ if (
+ field == this._elements.ccNumber &&
+ !FormAutofillUtils.isCCNumber(field.value)
+ ) {
+ let invalidCardNumberString =
+ this._elements.invalidCardNumberStringElement.textContent;
+ field.setCustomValidity(invalidCardNumberString || " ");
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/customElements.js b/browser/extensions/formautofill/content/customElements.js
new file mode 100644
index 0000000000..0b3d761817
--- /dev/null
+++ b/browser/extensions/formautofill/content/customElements.js
@@ -0,0 +1,410 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+(() => {
+ function sendMessageToBrowser(msgName, data) {
+ let { AutoCompleteParent } = ChromeUtils.importESModule(
+ "resource://gre/actors/AutoCompleteParent.sys.mjs"
+ );
+
+ let actor = AutoCompleteParent.getCurrentActor();
+ if (!actor) {
+ return;
+ }
+
+ actor.manager.getActor("FormAutofill").sendAsyncMessage(msgName, data);
+ }
+
+ class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem {
+ constructor() {
+ super();
+
+ /**
+ * For form autofill, we want to unify the selection no matter by
+ * keyboard navigation or mouseover in order not to confuse user which
+ * profile preview is being shown. This field is set to true to indicate
+ * that selectedIndex of popup should be changed while mouseover item
+ */
+ this.selectedByMouseOver = true;
+ }
+
+ get _stringBundle() {
+ if (!this.__stringBundle) {
+ this.__stringBundle = Services.strings.createBundle(
+ "chrome://formautofill/locale/formautofill.properties"
+ );
+ }
+ return this.__stringBundle;
+ }
+
+ _cleanup() {
+ this.removeAttribute("formautofillattached");
+ if (this._itemBox) {
+ this._itemBox.removeAttribute("size");
+ }
+ }
+
+ _onOverflow() {}
+
+ _onUnderflow() {}
+
+ handleOverUnderflow() {}
+
+ _adjustAutofillItemLayout() {
+ let outerBoxRect = this.parentNode.getBoundingClientRect();
+
+ // Make item fit in popup as XUL box could not constrain
+ // item's width
+ this._itemBox.style.width = outerBoxRect.width + "px";
+ // Use two-lines layout when width is smaller than 150px or
+ // 185px if an image precedes the label.
+ let oneLineMinRequiredWidth = this.getAttribute("ac-image") ? 185 : 150;
+
+ if (outerBoxRect.width <= oneLineMinRequiredWidth) {
+ this._itemBox.setAttribute("size", "small");
+ } else {
+ this._itemBox.removeAttribute("size");
+ }
+ }
+ }
+
+ MozElements.MozAutocompleteProfileListitem = class MozAutocompleteProfileListitem extends (
+ MozAutocompleteProfileListitemBase
+ ) {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box">
+ <div class="profile-label-col profile-item-col">
+ <span class="profile-label-affix"></span>
+ <span class="profile-label"></span>
+ </div>
+ <div class="profile-comment-col profile-item-col">
+ <span class="profile-comment"></span>
+ </div>
+ </div>
+ `;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-item-box");
+ this._labelAffix = this.querySelector(".profile-label-affix");
+ this._label = this.querySelector(".profile-label");
+ this._comment = this.querySelector(".profile-comment");
+
+ this.initializeAttributeInheritance();
+ this._adjustAcItem();
+ }
+
+ static get inheritedAttributes() {
+ return {
+ ".autofill-item-box": "ac-image",
+ };
+ }
+
+ set selected(val) {
+ if (val) {
+ this.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ }
+
+ sendMessageToBrowser("FormAutofill:PreviewProfile");
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+ this._itemBox.style.setProperty(
+ "--primary-icon",
+ `url(${this.getAttribute("ac-image")})`
+ );
+
+ let { primaryAffix, primary, secondary, ariaLabel } = JSON.parse(
+ this.getAttribute("ac-value")
+ );
+
+ this._labelAffix.textContent = primaryAffix;
+ this._label.textContent = primary;
+ this._comment.textContent = secondary;
+ if (ariaLabel) {
+ this.setAttribute("aria-label", ariaLabel);
+ }
+ }
+ };
+
+ customElements.define(
+ "autocomplete-profile-listitem",
+ MozElements.MozAutocompleteProfileListitem,
+ { extends: "richlistitem" }
+ );
+
+ class MozAutocompleteProfileListitemFooter extends MozAutocompleteProfileListitemBase {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer">
+ <div class="autofill-footer-row autofill-warning"></div>
+ <div class="autofill-footer-row autofill-button"></div>
+ </div>
+ `;
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("click", event => {
+ if (event.button != 0) {
+ return;
+ }
+
+ if (this._warningTextBox.contains(event.originalTarget)) {
+ return;
+ }
+
+ window.openPreferences("privacy-form-autofill");
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-footer");
+ this._optionButton = this.querySelector(".autofill-button");
+ this._warningTextBox = this.querySelector(".autofill-warning");
+
+ /**
+ * A handler for updating warning message once selectedIndex has been changed.
+ *
+ * There're three different states of warning message:
+ * 1. None of addresses were selected: We show all the categories intersection of fields in the
+ * form and fields in the results.
+ * 2. An address was selested: Show the additional categories that will also be filled.
+ * 3. An address was selected, but the focused category is the same as the only one category: Only show
+ * the exact category that we're going to fill in.
+ *
+ * @private
+ * @param {object} data
+ * Message data
+ * @param {string[]} data.categories
+ * The categories of all the fields contained in the selected address.
+ */
+ this.updateWarningNote = data => {
+ let categories =
+ data && data.categories ? data.categories : this._allFieldCategories;
+ // If the length of categories is 1, that means all the fillable fields are in the same
+ // category. We will change the way to inform user according to this flag. When the value
+ // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only.
+ let hasExtraCategories = categories.length > 1;
+ // Show the categories in certain order to conform with the spec.
+ let orderedCategoryList = [
+ { id: "address", l10nId: "category.address" },
+ { id: "name", l10nId: "category.name" },
+ { id: "organization", l10nId: "category.organization2" },
+ { id: "tel", l10nId: "category.tel" },
+ { id: "email", l10nId: "category.email" },
+ ];
+ let showCategories = hasExtraCategories
+ ? orderedCategoryList.filter(
+ category =>
+ categories.includes(category.id) &&
+ category.id != this._focusedCategory
+ )
+ : [
+ orderedCategoryList.find(
+ category => category.id == this._focusedCategory
+ ),
+ ];
+
+ let separator =
+ this._stringBundle.GetStringFromName("fieldNameSeparator");
+ let warningTextTmplKey = hasExtraCategories
+ ? "phishingWarningMessage"
+ : "phishingWarningMessage2";
+ let categoriesText = showCategories
+ .map(category =>
+ this._stringBundle.GetStringFromName(category.l10nId)
+ )
+ .join(separator);
+
+ this._warningTextBox.textContent =
+ this._stringBundle.formatStringFromName(warningTextTmplKey, [
+ categoriesText,
+ ]);
+ this.parentNode.parentNode.adjustHeight();
+ };
+
+ this._adjustAcItem();
+ }
+
+ _onCollapse() {
+ if (this.showWarningText) {
+ let { FormAutofillParent } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillParent.sys.mjs"
+ );
+ FormAutofillParent.removeMessageObserver(this);
+ }
+ this._itemBox.removeAttribute("no-warning");
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let buttonTextBundleKey;
+ if (this._itemBox.getAttribute("size") == "small") {
+ buttonTextBundleKey =
+ AppConstants.platform == "macosx"
+ ? "autocompleteFooterOptionOSXShort2"
+ : "autocompleteFooterOptionShort2";
+ } else {
+ buttonTextBundleKey =
+ AppConstants.platform == "macosx"
+ ? "autocompleteFooterOptionOSX2"
+ : "autocompleteFooterOption2";
+ }
+
+ let buttonText =
+ this._stringBundle.GetStringFromName(buttonTextBundleKey);
+ this._optionButton.textContent = buttonText;
+
+ let value = JSON.parse(this.getAttribute("ac-value"));
+
+ this._allFieldCategories = value.categories;
+ this._focusedCategory = value.focusedCategory;
+ this.showWarningText = this._allFieldCategories && this._focusedCategory;
+
+ if (this.showWarningText) {
+ let { FormAutofillParent } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillParent.sys.mjs"
+ );
+ FormAutofillParent.addMessageObserver(this);
+ this.updateWarningNote();
+ } else {
+ this._itemBox.setAttribute("no-warning", "true");
+ }
+ }
+ }
+
+ customElements.define(
+ "autocomplete-profile-listitem-footer",
+ MozAutocompleteProfileListitemFooter,
+ { extends: "richlistitem" }
+ );
+
+ class MozAutocompleteCreditcardInsecureField extends MozAutocompleteProfileListitemBase {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-insecure-item"></div>
+ `;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-insecure-item");
+
+ this._adjustAcItem();
+ }
+
+ set selected(val) {
+ // This item is unselectable since we see this item as a pure message.
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let value = this.getAttribute("ac-value");
+ this._itemBox.textContent = value;
+ }
+ }
+
+ customElements.define(
+ "autocomplete-creditcard-insecure-field",
+ MozAutocompleteCreditcardInsecureField,
+ { extends: "richlistitem" }
+ );
+
+ class MozAutocompleteProfileListitemClearButton extends MozAutocompleteProfileListitemBase {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer">
+ <div class="autofill-footer-row autofill-button"></div>
+ </div>
+ `;
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("click", event => {
+ if (event.button != 0) {
+ return;
+ }
+
+ sendMessageToBrowser("FormAutofill:ClearForm");
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-item-box");
+ this._clearBtn = this.querySelector(".autofill-button");
+
+ this._adjustAcItem();
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let clearFormBtnLabel =
+ this._stringBundle.GetStringFromName("clearFormBtnLabel2");
+ this._clearBtn.textContent = clearFormBtnLabel;
+ }
+ }
+
+ customElements.define(
+ "autocomplete-profile-listitem-clear-button",
+ MozAutocompleteProfileListitemClearButton,
+ { extends: "richlistitem" }
+ );
+})();
diff --git a/browser/extensions/formautofill/content/editAddress.xhtml b/browser/extensions/formautofill/content/editAddress.xhtml
new file mode 100644
index 0000000000..8972e75c47
--- /dev/null
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title data-l10n-id="autofill-add-new-address-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog-shared.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editAddress.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog.css"
+ />
+ <script src="chrome://formautofill/content/editDialog.js"></script>
+ <script src="chrome://formautofill/content/autofillEditForms.js"></script>
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-button-group.mjs"
+ ></script>
+ </head>
+ <body>
+ <form id="form" class="editAddressForm" autocomplete="off">
+ <!--
+ The <span class="label-text" …/> needs to be after the form field in the same element in
+ order to get proper label styling with :focus and :moz-ui-invalid.
+ -->
+ <div id="name-container" class="container">
+ <label id="given-name-container">
+ <input id="given-name" type="text" required="required" />
+ <span data-l10n-id="autofill-address-given-name" class="label-text" />
+ </label>
+ <label id="additional-name-container">
+ <input id="additional-name" type="text" />
+ <span
+ data-l10n-id="autofill-address-additional-name"
+ class="label-text"
+ />
+ </label>
+ <label id="family-name-container">
+ <input id="family-name" type="text" required="required" />
+ <span
+ data-l10n-id="autofill-address-family-name"
+ class="label-text"
+ />
+ </label>
+ </div>
+ <label id="organization-container" class="container">
+ <input id="organization" type="text" />
+ <span data-l10n-id="autofill-address-organization" class="label-text" />
+ </label>
+ <label id="street-address-container" class="container">
+ <textarea id="street-address" rows="3" />
+ <span data-l10n-id="autofill-address-street" class="label-text" />
+ </label>
+ <label id="address-level3-container" class="container">
+ <input id="address-level3" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="address-level2-container" class="container">
+ <input id="address-level2" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="address-level1-container" class="container">
+ <!-- The address-level1 input will get replaced by a select dropdown
+ by autofillEditForms.js when the selected country has provided
+ specific options. -->
+ <input id="address-level1" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="postal-code-container" class="container">
+ <input id="postal-code" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="country-container" class="container">
+ <select id="country" required="required">
+ <option />
+ </select>
+ <span data-l10n-id="autofill-address-country" class="label-text" />
+ </label>
+ <label id="tel-container" class="container">
+ <input id="tel" type="tel" dir="auto" />
+ <span data-l10n-id="autofill-address-tel" class="label-text" />
+ </label>
+ <label id="email-container" class="container">
+ <input id="email" type="email" required="required" />
+ <span data-l10n-id="autofill-address-email" class="label-text" />
+ </label>
+ </form>
+ <div id="controls-container">
+ <span
+ id="country-warning-message"
+ data-l10n-id="autofill-country-warning-message"
+ />
+ <moz-button-group>
+ <button id="cancel" data-l10n-id="autofill-cancel-button" />
+ <button id="save" class="primary" data-l10n-id="autofill-save-button" />
+ </moz-button-group>
+ </div>
+ <script>
+ <![CDATA[
+ "use strict";
+
+ const {
+ record,
+ noValidate,
+ } = window.arguments?.[0] ?? {};
+
+ /* import-globals-from autofillEditForms.js */
+ const fieldContainer = new EditAddress({
+ form: document.getElementById("form"),
+ }, record, {
+ noValidate,
+ });
+
+ /* import-globals-from editDialog.js */
+ new EditAddressDialog({
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ }, record);
+ ]]>
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/editCreditCard.xhtml b/browser/extensions/formautofill/content/editCreditCard.xhtml
new file mode 100644
index 0000000000..c8315540c6
--- /dev/null
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title data-l10n-id="autofill-add-new-card-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog-shared.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editCreditCard.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog.css"
+ />
+ <script src="chrome://formautofill/content/editDialog.js"></script>
+ <script src="chrome://formautofill/content/autofillEditForms.js"></script>
+ </head>
+ <body>
+ <form id="form" class="editCreditCardForm contentPane" autocomplete="off">
+ <!--
+ The <span class="label-text" …/> needs to be after the form field in the same element in
+ order to get proper label styling with :focus and :moz-ui-invalid.
+ -->
+ <label id="cc-number-container" class="container" role="none">
+ <span
+ id="invalidCardNumberString"
+ hidden="hidden"
+ data-l10n-id="autofill-card-invalid-number"
+ ></span>
+ <!-- Because there is text both before and after the input, a11y will
+ include the value of the input in the label. Therefore, we override
+ with aria-labelledby.
+ -->
+ <input
+ id="cc-number"
+ type="text"
+ required="required"
+ minlength="14"
+ pattern="[- 0-9]+"
+ aria-labelledby="cc-number-label"
+ />
+ <span
+ id="cc-number-label"
+ data-l10n-id="autofill-card-number"
+ class="label-text"
+ />
+ </label>
+ <label id="cc-exp-month-container" class="container">
+ <select id="cc-exp-month" required="required">
+ <option />
+ </select>
+ <span data-l10n-id="autofill-card-expires-month" class="label-text" />
+ </label>
+ <label id="cc-exp-year-container" class="container">
+ <select id="cc-exp-year" required="required">
+ <option />
+ </select>
+ <span data-l10n-id="autofill-card-expires-year" class="label-text" />
+ </label>
+ <label id="cc-name-container" class="container">
+ <input id="cc-name" type="text" required="required" />
+ <span data-l10n-id="autofill-card-name-on-card" class="label-text" />
+ </label>
+ <label id="cc-csc-container" class="container" hidden="hidden">
+ <!-- The CSC container will get filled in by forms that need a CSC (using csc-input.js) -->
+ </label>
+ <div
+ id="billingAddressGUID-container"
+ class="billingAddressRow container rich-picker"
+ >
+ <select id="billingAddressGUID" required="required"></select>
+ <label
+ for="billingAddressGUID"
+ data-l10n-id="autofill-card-billing-address"
+ class="label-text"
+ />
+ </div>
+ </form>
+ <div id="controls-container">
+ <button id="cancel" data-l10n-id="autofill-cancel-button" />
+ <button id="save" class="primary" data-l10n-id="autofill-save-button" />
+ </div>
+ <script>
+ <![CDATA[
+ "use strict";
+
+ /* import-globals-from editDialog.js */
+
+ (async () => {
+ const {
+ record,
+ } = window.arguments?.[0] ?? {};
+
+ const addresses = {};
+ for (let address of await formAutofillStorage.addresses.getAll()) {
+ addresses[address.guid] = address;
+ }
+
+ /* import-globals-from autofillEditForms.js */
+ const fieldContainer = new EditCreditCard({
+ form: document.getElementById("form"),
+ }, record, addresses);
+
+ new EditCreditCardDialog({
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ }, record);
+ })();
+ ]]>
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/editDialog.js b/browser/extensions/formautofill/content/editDialog.js
new file mode 100644
index 0000000000..77dcbb2ae0
--- /dev/null
+++ b/browser/extensions/formautofill/content/editDialog.js
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported EditAddressDialog, EditCreditCardDialog */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "AutofillTelemetry",
+ "resource://autofill/AutofillTelemetry.jsm"
+);
+
+class AutofillEditDialog {
+ constructor(subStorageName, elements, record) {
+ this._storageInitPromise = formAutofillStorage.initialize();
+ this._subStorageName = subStorageName;
+ this._elements = elements;
+ this._record = record;
+ this.localizeDocument();
+ window.addEventListener("DOMContentLoaded", this, { once: true });
+ }
+
+ async init() {
+ this.updateSaveButtonState();
+ this.attachEventListeners();
+ // For testing only: signal to tests that the dialog is ready for testing.
+ // This is likely no longer needed since retrieving from storage is fully
+ // handled in manageDialog.js now.
+ window.dispatchEvent(new CustomEvent("FormReady"));
+ }
+
+ /**
+ * Get storage and ensure it has been initialized.
+ *
+ * @returns {object}
+ */
+ async getStorage() {
+ await this._storageInitPromise;
+ return formAutofillStorage[this._subStorageName];
+ }
+
+ /**
+ * Asks FormAutofillParent to save or update an record.
+ *
+ * @param {object} record
+ * @param {string} guid [optional]
+ */
+ async saveRecord(record, guid) {
+ let storage = await this.getStorage();
+ if (guid) {
+ await storage.update(guid, record);
+ } else {
+ await storage.add(record);
+ }
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.init();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ case "keypress": {
+ this.handleKeyPress(event);
+ break;
+ }
+ case "contextmenu": {
+ if (
+ !HTMLInputElement.isInstance(event.target) &&
+ !HTMLTextAreaElement.isInstance(event.target)
+ ) {
+ event.preventDefault();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.cancel) {
+ window.close();
+ }
+ if (event.target == this._elements.save) {
+ this.handleSubmit();
+ }
+ }
+
+ /**
+ * Handle input events
+ *
+ * @param {DOMEvent} event
+ */
+ handleInput(event) {
+ this.updateSaveButtonState();
+ }
+
+ /**
+ * Handle key press events
+ *
+ * @param {DOMEvent} event
+ */
+ handleKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ }
+
+ updateSaveButtonState() {
+ // Toggle disabled attribute on the save button based on
+ // whether the form is filled or empty.
+ if (!Object.keys(this._elements.fieldContainer.buildFormObject()).length) {
+ this._elements.save.setAttribute("disabled", true);
+ } else {
+ this._elements.save.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("keypress", this);
+ window.addEventListener("contextmenu", this);
+ this._elements.controlsContainer.addEventListener("click", this);
+ document.addEventListener("input", this);
+ }
+
+ // An interface to be inherited.
+ localizeDocument() {}
+
+ recordFormSubmit() {
+ let method = this._record?.guid ? "edit" : "add";
+ AutofillTelemetry.recordManageEvent(this.telemetryType, method);
+ }
+}
+
+class EditAddressDialog extends AutofillEditDialog {
+ telemetryType = AutofillTelemetry.ADDRESS;
+
+ constructor(elements, record) {
+ super("addresses", elements, record);
+ if (record) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ }
+ }
+
+ localizeDocument() {
+ if (this._record?.guid) {
+ document.l10n.setAttributes(
+ this._elements.title,
+ "autofill-edit-address-title"
+ );
+ }
+ }
+
+ async handleSubmit() {
+ await this.saveRecord(
+ this._elements.fieldContainer.buildFormObject(),
+ this._record ? this._record.guid : null
+ );
+ this.recordFormSubmit();
+
+ window.close();
+ }
+}
+
+class EditCreditCardDialog extends AutofillEditDialog {
+ telemetryType = AutofillTelemetry.CREDIT_CARD;
+
+ constructor(elements, record) {
+ elements.fieldContainer._elements.billingAddress.disabled = true;
+ super("creditCards", elements, record);
+ elements.fieldContainer._elements.ccNumber.addEventListener(
+ "blur",
+ this._onCCNumberFieldBlur.bind(this)
+ );
+ if (record) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ }
+ }
+
+ _onCCNumberFieldBlur() {
+ let elem = this._elements.fieldContainer._elements.ccNumber;
+ this._elements.fieldContainer.updateCustomValidity(elem);
+ }
+
+ localizeDocument() {
+ if (this._record?.guid) {
+ document.l10n.setAttributes(
+ this._elements.title,
+ "autofill-edit-card-title"
+ );
+ }
+ }
+
+ async handleSubmit() {
+ let creditCard = this._elements.fieldContainer.buildFormObject();
+ if (!this._elements.fieldContainer._elements.form.reportValidity()) {
+ return;
+ }
+
+ try {
+ await this.saveRecord(
+ creditCard,
+ this._record ? this._record.guid : null
+ );
+
+ this.recordFormSubmit();
+
+ window.close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/formautofill.css b/browser/extensions/formautofill/content/formautofill.css
new file mode 100644
index 0000000000..fad9ee410a
--- /dev/null
+++ b/browser/extensions/formautofill/content/formautofill.css
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"] {
+ display: block;
+ margin: 0;
+ padding: 0;
+ height: auto;
+ min-height: auto;
+}
+
+/* Treat @collpased="true" as display: none similar to how it is for XUL elements.
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"][collapsed="true"] {
+ display: none;
+}
+
+#PopupAutoComplete[resultstyles~="autofill-profile"] {
+ min-width: 150px !important;
+}
+
+#PopupAutoComplete[resultstyles~="autofill-insecureWarning"] {
+ min-width: 200px !important;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[disabled="true"] {
+ opacity: 0.5;
+}
+
+/* Form Autofill Doorhanger */
+#autofill-address-notification popupnotificationcontent > .desc-message-box,
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box {
+ margin-block-end: 12px;
+}
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box > image {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: auto;
+ height: auto;
+ list-style-image: url(chrome://formautofill/content/icon-credit-card-generic.svg);
+}
+#autofill-address-notification popupnotificationcontent > .desc-message-box > description,
+#autofill-address-notification popupnotificationcontent > .desc-message-box > additional-description,
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box > description {
+ font-style: italic;
+ margin-inline-start: 4px;
+}
diff --git a/browser/extensions/formautofill/content/formfill-anchor.svg b/browser/extensions/formautofill/content/formfill-anchor.svg
new file mode 100644
index 0000000000..0a9ef19add
--- /dev/null
+++ b/browser/extensions/formautofill/content/formfill-anchor.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M7.3 6h1.5c.1 0 .2-.1.2-.3V2c0-.5-.4-1-1-1s-1 .4-1 1v3.8c0 .1.1.2.3.2z"/>
+ <path d="M13.5 3H11c-.6 0-1 .4-1 1s.4 1 1 1h2.5c.3 0 .5.2.5.5v7c0 .3-.2.5-.5.5h-11c-.3 0-.5-.3-.5-.5v-7c0-.3.2-.5.5-.5H5c.6 0 1-.4 1-1s-.4-1-1-1H2.5C1.1 3 0 4.1 0 5.5v7C0 13.8 1.1 15 2.5 15h11c1.4 0 2.5-1.1 2.5-2.5v-7C16 4.1 14.9 3 13.5 3z"/>
+ <path d="M3.6 7h2.8c.3 0 .6.2.6.5v2.8c0 .4-.3.7-.6.7H3.6c-.3 0-.6-.3-.6-.6V7.5c0-.3.3-.5.6-.5zM9.5 8h3c.3 0 .5-.3.5-.5s-.2-.5-.5-.5h-3c-.3 0-.5.2-.5.5s.2.5.5.5zM9.5 9c-.3 0-.5.2-.5.5s.2.5.5.5h2c.3 0 .5-.2.5-.5s-.2-.5-.5-.5h-2z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-address-save.svg b/browser/extensions/formautofill/content/icon-address-save.svg
new file mode 100644
index 0000000000..8fdcf1cd5f
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-address-save.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 32 32">
+ <path d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm21.1-1.9h-2.5V20c0-.4-.3-.8-.8-.8h-3.1c-.4 0-.8.3-.8.8v4.6h-2.5c-.6 0-.8.4-.3.8l4.3 4.2c.2.2.5.3.8.3s.6-.1.8-.3l4.3-4.2c.6-.4.4-.7-.2-.7zm-11.3-5.6H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-address-update.svg b/browser/extensions/formautofill/content/icon-address-update.svg
new file mode 100644
index 0000000000..1455423fed
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-address-update.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 32 32">
+ <path d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm9.8-7.5H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zm-5.7 16l4.4-4.3c.2-.2.5-.3.8-.3s.6.1.8.3l4.4 4.3c.5.5.3.8-.3.8h-2.6v4.7c0 .4-.4.8-.8.8h-3c-.4 0-.8-.4-.8-.8v-4.7h-2.5c-.7 0-.8-.4-.4-.8z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-credit-card-generic.svg b/browser/extensions/formautofill/content/icon-credit-card-generic.svg
new file mode 100644
index 0000000000..5d554fe7ce
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-credit-card-generic.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" viewBox="0 0 16 16">
+ <path d="M4.5,9.4H3.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h1.3c0.3,0,0.5-0.2,0.5-0.5S4.8,9.4,4.5,9.4z"/>
+ <path d="M9.3,9.4H6.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h3.2c0.3,0,0.5-0.2,0.5-0.5S9.6,9.4,9.3,9.4z"/>
+ <path d="M14,2H2C0.9,2,0,2.9,0,4v8c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V4C16,2.9,15.1,2,14,2z M14,12H2V7.7h12V12z M14,6H2V4h12V6z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-credit-card.svg b/browser/extensions/formautofill/content/icon-credit-card.svg
new file mode 100644
index 0000000000..7ec782f880
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-credit-card.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 32 32">
+ <path d="M9 22.2H6.4c-.6 0-1 .4-1 1s.4 1 1 1H9c.6 0 1-.4 1-1s-.4-1-1-1z"/>
+ <path d="M28 7.6v8H4v-4h10v-4H4c-2.2 0-4 1.8-4 4v16c0 2.2 1.8 4 4 4h24c2.2 0 4-1.8 4-4v-16c0-2.2-1.8-4-4-4zm-24 20V19h24v8.6H4z"/>
+ <path d="M19.2 22.2h-6.3c-.6 0-1 .4-1 1s.4 1 1 1h6.3c.6 0 1-.4 1-1s-.5-1-1-1zM16.3 7.9c-.4.4-.4 1 0 1.4l4 4c.4.4 1 .4 1.4 0l4-4c.4-.4.4-1 0-1.4s-1-.4-1.4 0L22 10.2v-9c0-.5-.4-1-1-1-.5 0-1 .4-1 1v9l-2.3-2.3c-.4-.4-1-.4-1.4 0z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/manageAddresses.xhtml b/browser/extensions/formautofill/content/manageAddresses.xhtml
new file mode 100644
index 0000000000..68e810179e
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageAddresses.xhtml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ data-l10n-id="autofill-manage-dialog"
+ data-l10n-attrs="style"
+>
+ <head>
+ <title data-l10n-id="autofill-manage-addresses-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/manageDialog.css"
+ />
+ <script src="chrome://formautofill/content/manageDialog.js"></script>
+ </head>
+ <body>
+ <fieldset>
+ <legend data-l10n-id="autofill-manage-addresses-list-header" />
+ <select id="addresses" size="9" multiple="multiple" />
+ </fieldset>
+ <div id="controls-container">
+ <button
+ id="remove"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-remove-button"
+ />
+ <!-- Wrapper is used to properly compute the search tooltip position -->
+ <div>
+ <button id="add" data-l10n-id="autofill-manage-add-button" />
+ </div>
+ <button
+ id="edit"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-edit-button"
+ />
+ </div>
+ <script>
+ "use strict";
+ /* global ManageAddresses */
+ new ManageAddresses({
+ records: document.getElementById("addresses"),
+ controlsContainer: document.getElementById("controls-container"),
+ remove: document.getElementById("remove"),
+ add: document.getElementById("add"),
+ edit: document.getElementById("edit"),
+ });
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/manageCreditCards.xhtml b/browser/extensions/formautofill/content/manageCreditCards.xhtml
new file mode 100644
index 0000000000..3e5bdcfbf5
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ data-l10n-id="autofill-manage-dialog"
+ data-l10n-attrs="style"
+>
+ <head>
+ <title data-l10n-id="autofill-manage-credit-cards-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link rel="localization" href="toolkit/payments/payments.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/manageDialog.css"
+ />
+ <script src="chrome://formautofill/content/manageDialog.js"></script>
+ </head>
+ <body>
+ <fieldset>
+ <legend data-l10n-id="autofill-manage-credit-cards-list-header" />
+ <select id="credit-cards" size="9" multiple="multiple" />
+ </fieldset>
+ <div id="controls-container">
+ <button
+ id="remove"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-remove-button"
+ />
+ <!-- Wrapper is used to properly compute the search tooltip position -->
+ <div>
+ <button id="add" data-l10n-id="autofill-manage-add-button" />
+ </div>
+ <button
+ id="edit"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-edit-button"
+ />
+ </div>
+ <script>
+ "use strict";
+ /* global ManageCreditCards */
+ new ManageCreditCards({
+ records: document.getElementById("credit-cards"),
+ controlsContainer: document.getElementById("controls-container"),
+ remove: document.getElementById("remove"),
+ add: document.getElementById("add"),
+ edit: document.getElementById("edit"),
+ });
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/manageDialog.css b/browser/extensions/formautofill/content/manageDialog.css
new file mode 100644
index 0000000000..f347c79118
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageDialog.css
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+ /* Prevent unnecessary horizontal scroll bar from showing */
+ overflow-x: hidden;
+}
+
+div {
+ display: flex;
+}
+
+button {
+ padding-inline: 10px;
+}
+
+fieldset {
+ margin: 0 4px;
+ padding: 0;
+ border: none;
+}
+
+fieldset > legend {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0.4em 0.7em;
+ background-color: var(--in-content-box-background);
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: 2px 2px 0 0;
+ user-select: none;
+}
+
+option:nth-child(even) {
+ background-color: var(--in-content-box-background-odd);
+}
+
+#addresses,
+#credit-cards {
+ width: 100%;
+ height: 16.6em;
+ margin: 0;
+ padding-inline: 0;
+ border-top: none;
+ border-radius: 0 0 2px 2px;
+}
+
+#addresses > option,
+#credit-cards > option {
+ display: flex;
+ align-items: center;
+ height: 1.6em;
+ padding-inline-start: 0.6em;
+}
+
+#controls-container {
+ margin-top: 1em;
+}
+
+#remove {
+ margin-inline-end: auto;
+}
+
+#credit-cards > option::before {
+ content: "";
+ background: url("icon-credit-card-generic.svg") no-repeat;
+ background-size: contain;
+ float: inline-start;
+ width: 16px;
+ height: 16px;
+ padding-inline-end: 10px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+/*
+ We use .png / @2x.png images where we don't yet have a vector version of a logo
+*/
+#credit-cards.branded > option[cc-type="amex"]::before {
+ background-image: url("third-party/cc-logo-amex.png");
+}
+
+#credit-cards.branded > option[cc-type="cartebancaire"]::before {
+ background-image: url("third-party/cc-logo-cartebancaire.png");
+}
+
+#credit-cards.branded > option[cc-type="diners"]::before {
+ background-image: url("third-party/cc-logo-diners.svg");
+}
+
+#credit-cards.branded > option[cc-type="discover"]::before {
+ background-image: url("third-party/cc-logo-discover.png");
+}
+
+#credit-cards.branded > option[cc-type="jcb"]::before {
+ background-image: url("third-party/cc-logo-jcb.svg");
+}
+
+#credit-cards.branded > option[cc-type="mastercard"]::before {
+ background-image: url("third-party/cc-logo-mastercard.svg");
+}
+
+#credit-cards.branded > option[cc-type="mir"]::before {
+ background-image: url("third-party/cc-logo-mir.svg");
+}
+
+#credit-cards.branded > option[cc-type="unionpay"]::before {
+ background-image: url("third-party/cc-logo-unionpay.svg");
+}
+
+#credit-cards.branded > option[cc-type="visa"]::before {
+ background-image: url("third-party/cc-logo-visa.svg");
+}
+
+@media (min-resolution: 1.1dppx) {
+ #credit-cards.branded > option[cc-type="amex"]::before {
+ background-image: url("third-party/cc-logo-amex@2x.png");
+ }
+ #credit-cards.branded > option[cc-type="cartebancaire"]::before {
+ background-image: url("third-party/cc-logo-cartebancaire@2x.png");
+ }
+ #credit-cards.branded > option[cc-type="discover"]::before {
+ background-image: url("third-party/cc-logo-discover@2x.png");
+ }
+}
diff --git a/browser/extensions/formautofill/content/manageDialog.js b/browser/extensions/formautofill/content/manageDialog.js
new file mode 100644
index 0000000000..25498fbcaf
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -0,0 +1,464 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported ManageAddresses, ManageCreditCards */
+
+"use strict";
+
+const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
+const EDIT_CREDIT_CARD_URL =
+ "chrome://formautofill/content/editCreditCard.xhtml";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+const { AutofillTelemetry } = ChromeUtils.import(
+ "resource://autofill/AutofillTelemetry.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+});
+
+const lazy = {};
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () =>
+ new Localization([
+ "browser/preferences/formAutofill.ftl",
+ "branding/brand.ftl",
+ ])
+);
+
+this.log = null;
+XPCOMUtils.defineLazyGetter(this, "log", () =>
+ FormAutofill.defineLogGetter(this, "manageAddresses")
+);
+
+class ManageRecords {
+ constructor(subStorageName, elements) {
+ this._storageInitPromise = formAutofillStorage.initialize();
+ this._subStorageName = subStorageName;
+ this._elements = elements;
+ this._newRequest = false;
+ this._isLoadingRecords = false;
+ this.prefWin = window.opener;
+ window.addEventListener("DOMContentLoaded", this, { once: true });
+ }
+
+ async init() {
+ await this.loadRecords();
+ this.attachEventListeners();
+ // For testing only: Notify when the dialog is ready for interaction
+ window.dispatchEvent(new CustomEvent("FormReady"));
+ }
+
+ uninit() {
+ log.debug("uninit");
+ this.detachEventListeners();
+ this._elements = null;
+ }
+
+ /**
+ * Get the selected options on the addresses element.
+ *
+ * @returns {Array<DOMElement>}
+ */
+ get _selectedOptions() {
+ return Array.from(this._elements.records.selectedOptions);
+ }
+
+ /**
+ * Get storage and ensure it has been initialized.
+ *
+ * @returns {object}
+ */
+ async getStorage() {
+ await this._storageInitPromise;
+ return formAutofillStorage[this._subStorageName];
+ }
+
+ /**
+ * Load records and render them. This function is a wrapper for _loadRecords
+ * to ensure any reentrant will be handled well.
+ */
+ async loadRecords() {
+ // This function can be early returned when there is any reentrant happends.
+ // "_newRequest" needs to be set to ensure all changes will be applied.
+ if (this._isLoadingRecords) {
+ this._newRequest = true;
+ return;
+ }
+ this._isLoadingRecords = true;
+
+ await this._loadRecords();
+
+ // _loadRecords should be invoked again if there is any multiple entrant
+ // during running _loadRecords(). This step ensures that the latest request
+ // still is applied.
+ while (this._newRequest) {
+ this._newRequest = false;
+ await this._loadRecords();
+ }
+ this._isLoadingRecords = false;
+
+ // For testing only: Notify when records are loaded
+ this._elements.records.dispatchEvent(new CustomEvent("RecordsLoaded"));
+ }
+
+ async _loadRecords() {
+ let storage = await this.getStorage();
+ let records = await storage.getAll();
+ // Sort by last used time starting with most recent
+ records.sort((a, b) => {
+ let aLastUsed = a.timeLastUsed || a.timeLastModified;
+ let bLastUsed = b.timeLastUsed || b.timeLastModified;
+ return bLastUsed - aLastUsed;
+ });
+ await this.renderRecordElements(records);
+ this.updateButtonsStates(this._selectedOptions.length);
+ }
+
+ /**
+ * Render the records onto the page while maintaining selected options if
+ * they still exist.
+ *
+ * @param {Array<object>} records
+ */
+ async renderRecordElements(records) {
+ let selectedGuids = this._selectedOptions.map(option => option.value);
+ this.clearRecordElements();
+ for (let record of records) {
+ let { id, args, raw } = await this.getLabelInfo(record);
+ let option = new Option(
+ raw ?? "",
+ record.guid,
+ false,
+ selectedGuids.includes(record.guid)
+ );
+ if (id) {
+ document.l10n.setAttributes(option, id, args);
+ }
+
+ option.record = record;
+ this._elements.records.appendChild(option);
+ }
+ }
+
+ /**
+ * Remove all existing record elements.
+ */
+ clearRecordElements() {
+ let parent = this._elements.records;
+ while (parent.lastChild) {
+ parent.removeChild(parent.lastChild);
+ }
+ }
+
+ /**
+ * Remove records by selected options.
+ *
+ * @param {Array<DOMElement>} options
+ */
+ async removeRecords(options) {
+ let storage = await this.getStorage();
+ // Pause listening to storage change event to avoid triggering `loadRecords`
+ // when removing records
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+
+ for (let option of options) {
+ storage.remove(option.value);
+ option.remove();
+ }
+ this.updateButtonsStates(this._selectedOptions);
+
+ // Resume listening to storage change event
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ // For testing only: notify record(s) has been removed
+ this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
+
+ for (let i = 0; i < options.length; i++) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "delete");
+ }
+ }
+
+ /**
+ * Enable/disable the Edit and Remove buttons based on number of selected
+ * options.
+ *
+ * @param {number} selectedCount
+ */
+ updateButtonsStates(selectedCount) {
+ log.debug("updateButtonsStates:", selectedCount);
+ if (selectedCount == 0) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.setAttribute("disabled", "disabled");
+ } else if (selectedCount == 1) {
+ this._elements.edit.removeAttribute("disabled");
+ this._elements.remove.removeAttribute("disabled");
+ } else if (selectedCount > 1) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.init();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "change": {
+ this.updateButtonsStates(this._selectedOptions.length);
+ break;
+ }
+ case "unload": {
+ this.uninit();
+ break;
+ }
+ case "keypress": {
+ this.handleKeyPress(event);
+ break;
+ }
+ case "contextmenu": {
+ event.preventDefault();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.remove) {
+ this.removeRecords(this._selectedOptions);
+ } else if (event.target == this._elements.add) {
+ this.openEditDialog();
+ } else if (
+ event.target == this._elements.edit ||
+ (event.target.parentNode == this._elements.records && event.detail > 1)
+ ) {
+ this.openEditDialog(this._selectedOptions[0].record);
+ }
+ }
+
+ /**
+ * Handle key press events
+ *
+ * @param {DOMEvent} event
+ */
+ handleKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ if (event.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.removeRecords(this._selectedOptions);
+ }
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "formautofill-storage-changed": {
+ this.loadRecords();
+ }
+ }
+ }
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("unload", this, { once: true });
+ window.addEventListener("keypress", this);
+ window.addEventListener("contextmenu", this);
+ this._elements.records.addEventListener("change", this);
+ this._elements.records.addEventListener("click", this);
+ this._elements.controlsContainer.addEventListener("click", this);
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ }
+
+ /**
+ * Remove event listener
+ */
+ detachEventListeners() {
+ window.removeEventListener("keypress", this);
+ window.removeEventListener("contextmenu", this);
+ this._elements.records.removeEventListener("change", this);
+ this._elements.records.removeEventListener("click", this);
+ this._elements.controlsContainer.removeEventListener("click", this);
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+ }
+}
+
+class ManageAddresses extends ManageRecords {
+ telemetryType = AutofillTelemetry.ADDRESS;
+
+ constructor(elements) {
+ super("addresses", elements);
+ elements.add.setAttribute(
+ "search-l10n-ids",
+ FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
+ );
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
+ }
+
+ /**
+ * Open the edit address dialog to create/edit an address.
+ *
+ * @param {object} address [optional]
+ */
+ openEditDialog(address) {
+ this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, undefined, {
+ record: address,
+ // Don't validate in preferences since it's fine for fields to be missing
+ // for autofill purposes. For PaymentRequest addresses get more validation.
+ noValidate: true,
+ });
+ }
+
+ getLabelInfo(address) {
+ return { raw: FormAutofillUtils.getAddressLabel(address) };
+ }
+}
+
+class ManageCreditCards extends ManageRecords {
+ telemetryType = AutofillTelemetry.CREDIT_CARD;
+
+ constructor(elements) {
+ super("creditCards", elements);
+ elements.add.setAttribute(
+ "search-l10n-ids",
+ FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
+ );
+
+ this._isDecrypted = false;
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
+ }
+
+ /**
+ * Open the edit address dialog to create/edit a credit card.
+ *
+ * @param {object} creditCard [optional]
+ */
+ async openEditDialog(creditCard) {
+ // Ask for reauth if user is trying to edit an existing credit card.
+ if (creditCard) {
+ const reauthPasswordPromptMessage = await lazy.l10n.formatValue(
+ "autofill-edit-card-password-prompt"
+ );
+ const loggedIn = await FormAutofillUtils.ensureLoggedIn(
+ reauthPasswordPromptMessage
+ );
+ if (!loggedIn.authenticated) {
+ return;
+ }
+ }
+
+ let decryptedCCNumObj = {};
+ if (creditCard && creditCard["cc-number-encrypted"]) {
+ try {
+ decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(
+ creditCard["cc-number-encrypted"]
+ );
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ABORT) {
+ // User shouldn't be ask to reauth here, but it could happen.
+ // Return here and skip opening the dialog.
+ return;
+ }
+ // We've got ourselves a real error.
+ // Recover from encryption error so the user gets a chance to re-enter
+ // unencrypted credit card number.
+ decryptedCCNumObj["cc-number"] = "";
+ console.error(ex);
+ }
+ }
+ let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
+ this.prefWin.gSubDialog.open(
+ EDIT_CREDIT_CARD_URL,
+ { features: "resizable=no" },
+ {
+ record: decryptedCreditCard,
+ }
+ );
+ }
+
+ /**
+ * Get credit card display label. It should display masked numbers and the
+ * cardholder's name, separated by a comma.
+ *
+ * @param {object} creditCard
+ * @returns {Promise<string>}
+ */
+ async getLabelInfo(creditCard) {
+ // The card type is displayed visually using an image. For a11y, we need
+ // to expose it as text. We do this using aria-label. However,
+ // aria-label overrides the text content, so we must include that also.
+ // Since the text content is generated by Fluent, aria-label must be
+ // generated by Fluent also.
+ const type = creditCard["cc-type"];
+ const typeL10nId = CreditCard.getNetworkL10nId(type);
+ const typeName = typeL10nId
+ ? await document.l10n.formatValue(typeL10nId)
+ : type ?? ""; // Unknown card type
+ return CreditCard.getLabelInfo({
+ name: creditCard["cc-name"],
+ number: creditCard["cc-number"],
+ month: creditCard["cc-exp-month"],
+ year: creditCard["cc-exp-year"],
+ type: typeName,
+ });
+ }
+
+ async renderRecordElements(records) {
+ // Revert back to encrypted form when re-rendering happens
+ this._isDecrypted = false;
+ // Display third-party card icons when possible
+ this._elements.records.classList.toggle(
+ "branded",
+ AppConstants.MOZILLA_OFFICIAL
+ );
+ await super.renderRecordElements(records);
+
+ let options = this._elements.records.options;
+ for (let option of options) {
+ let record = option.record;
+ if (record && record["cc-type"]) {
+ option.setAttribute("cc-type", record["cc-type"]);
+ } else {
+ option.removeAttribute("cc-type");
+ }
+ }
+ }
+
+ updateButtonsStates(selectedCount) {
+ super.updateButtonsStates(selectedCount);
+ }
+
+ handleClick(event) {
+ super.handleClick(event);
+ }
+}
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-amex.png b/browser/extensions/formautofill/content/third-party/cc-logo-amex.png
new file mode 100644
index 0000000000..c51a5be4a0
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-amex.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png b/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png
new file mode 100644
index 0000000000..f794641f3e
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png
new file mode 100644
index 0000000000..781c6e4958
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png
new file mode 100644
index 0000000000..38158846dd
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-diners.svg b/browser/extensions/formautofill/content/third-party/cc-logo-diners.svg
new file mode 100644
index 0000000000..9cc4d8b9ff
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-diners.svg
@@ -0,0 +1 @@
+<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M19.863 20.068c4.698.022 8.987-3.839 8.987-8.536 0-5.137-4.289-8.688-8.987-8.686h-4.044c-4.755-.002-8.669 3.55-8.669 8.686 0 4.698 3.914 8.559 8.669 8.536h4.044z" fill="#4186CD"/><path d="M15.76 3.535a7.923 7.923 0 0 0 0 15.844 7.923 7.923 0 0 0 0-15.844zm-4.821 7.75c.004-2.122 1.288-3.931 3.1-4.65v9.3c-1.812-.719-3.096-2.527-3.1-4.65zm6.544 4.65v-9.3c1.811.717 3.097 2.527 3.1 4.65-.003 2.123-1.289 3.931-3.1 4.65z" fill="#FFF"/><g fill="#211E1F"><path d="M.65 22.925c0-.71-.375-.663-.733-.671v-.205c.31.015.63.015.94.015.336 0 .79-.015 1.381-.015 2.065 0 3.19 1.365 3.19 2.763 0 .782-.462 2.748-3.286 2.748-.407 0-.782-.016-1.157-.016-.358 0-.71.008-1.068.016v-.205c.478-.048.71-.064.733-.6v-3.83zm.644 3.636c0 .586.437.654.825.654 1.713 0 2.275-1.24 2.275-2.373 0-1.422-.951-2.449-2.48-2.449-.326 0-.476.022-.62.03v4.138zM5.428 27.364h.152c.225 0 .387 0 .387-.25v-2.041c0-.332-.121-.378-.419-.528v-.12c.378-.107.83-.249.861-.272a.301.301 0 0 1 .145-.038c.04 0 .057.046.057.106v2.893c0 .25.177.25.402.25h.137v.196c-.274 0-.556-.015-.845-.015-.29 0-.58.007-.877.015v-.196zm.689-4.627a.36.36 0 0 1-.345-.35c0-.177.169-.338.345-.338.182 0 .344.148.344.337 0 .19-.155.351-.344.351zM7.993 25.117c0-.278-.084-.353-.438-.496v-.143c.325-.106.634-.204.996-.363.022 0 .045.016.045.076v.49c.43-.309.8-.566 1.307-.566.64 0 .867.468.867 1.055v1.944c0 .25.166.25.377.25h.136v.196c-.265 0-.528-.015-.8-.015s-.544.007-.815.015v-.196h.136c.211 0 .362 0 .362-.25v-1.95c0-.43-.263-.642-.694-.642-.241 0-.626.196-.876.362v2.23c0 .25.166.25.378.25h.136v.196c-.264 0-.529-.015-.8-.015-.272 0-.544.007-.816.015v-.196h.137c.21 0 .362 0 .362-.25v-1.997zM11.943 25.569c-.017.072-.017.192 0 .465.049.762.553 1.388 1.212 1.388.453 0 .809-.24 1.113-.537l.115.113c-.38.489-.849.906-1.525.906-1.31 0-1.575-1.236-1.575-1.75 0-1.573 1.089-2.039 1.665-2.039.668 0 1.386.41 1.394 1.26 0 .05 0 .097-.008.145l-.074.049h-2.317zm1.514-.42c.212 0 .237-.077.237-.147 0-.3-.264-.542-.742-.542-.52 0-.877.264-.98.689h1.485zM14.383 27.364h.191c.198 0 .34 0 .34-.25v-2.117c0-.233-.262-.279-.368-.339v-.113c.516-.234.799-.43.863-.43.042 0 .063.023.063.099v.678h.015c.176-.294.474-.777.905-.777.176 0 .402.128.402.4 0 .203-.133.385-.331.385-.22 0-.22-.182-.468-.182-.12 0-.516.174-.516.626v1.77c0 .25.142.25.34.25h.395v.196c-.389-.008-.684-.015-.99-.015-.289 0-.586.007-.84.015v-.196zM17.282 26.668c.102.53.418.98.996.98.465 0 .64-.29.64-.57 0-.948-1.724-.643-1.724-1.935 0-.45.357-1.028 1.226-1.028.252 0 .592.073.9.234l.056.818h-.182c-.079-.505-.355-.795-.862-.795-.316 0-.616.185-.616.53 0 .94 1.834.65 1.834 1.91 0 .53-.42 1.092-1.36 1.092a2.06 2.06 0 0 1-.964-.272l-.087-.924.143-.04zM26.431 23.625h-.192c-.147-.94-.786-1.318-1.649-1.318-.886 0-2.173.618-2.173 2.548 0 1.626 1.11 2.792 2.296 2.792.763 0 1.395-.547 1.55-1.392l.176.048-.177 1.175c-.323.21-1.194.426-1.703.426-1.802 0-2.942-1.214-2.942-3.024 0-1.649 1.41-2.831 2.92-2.831.623 0 1.224.21 1.817.427l.077 1.15zM26.783 27.36h.153c.226 0 .387 0 .387-.253v-4.268c0-.498-.12-.514-.427-.598v-.123c.322-.099.66-.237.83-.33.087-.045.152-.084.176-.084.05 0 .065.046.065.108v5.295c0 .254.177.254.403.254h.136v.199c-.273 0-.555-.016-.845-.016-.29 0-.58.008-.878.016v-.2zM31.775 27.032c0 .136.084.143.214.143.092 0 .206-.007.305-.007v.159c-.328.03-.955.188-1.1.233l-.038-.023v-.61c-.458.37-.81.633-1.353.633-.412 0-.84-.264-.84-.897v-1.93c0-.196-.03-.384-.457-.422v-.143c.275-.007.885-.053.984-.053.085 0 .085.053.085.219v1.944c0 .226 0 .874.664.874.26 0 .604-.195.924-.458v-2.029c0-.15-.366-.233-.64-.309v-.135c.686-.046 1.115-.106 1.19-.106.062 0 .062.053.062.136v2.78zM33.372 24.72c.323-.27.76-.572 1.206-.572.94 0 1.505.804 1.505 1.671 0 1.042-.776 2.085-1.935 2.085-.599 0-.914-.191-1.125-.278l-.243.182-.169-.087a9.26 9.26 0 0 0 .113-1.416v-3.422c0-.518-.122-.534-.43-.621v-.128c.325-.103.664-.246.834-.342.09-.048.154-.088.18-.088.047 0 .064.048.064.112v2.905zm-.044 2.032c0 .301.291.808.834.808.868 0 1.232-.831 1.232-1.535 0-.854-.664-1.565-1.296-1.565-.3 0-.552.19-.77.372v1.92z"/></g></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-discover.png b/browser/extensions/formautofill/content/third-party/cc-logo-discover.png
new file mode 100644
index 0000000000..104f9ee2d6
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-discover.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png b/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png
new file mode 100644
index 0000000000..1caaa01995
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg b/browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg
new file mode 100644
index 0000000000..5cdbb027f8
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg
@@ -0,0 +1 @@
+<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg"><defs><path d="M3.622.575C1.734.575.009 2.278.009 4.188c0 1.051 0 5.212-.002 9.348.346.217 2.01.752 2.68.799 1.466.103 2.375-.381 2.515-1.603l-.007-4.538h3.243v4.434c-.17 2.54-2.399 3.057-5.79 2.887-.877-.046-2.07-.27-2.64-.42L.004 22.94H5.65c1.54 0 3.544-1.439 3.544-3.627V.575H3.622z" id="a"/><linearGradient x1="-.003%" y1="49.999%" x2="100.002%" y2="49.999%" id="b"><stop stop-color="#313477" offset="0%"/><stop stop-color="#0077BC" offset="100%"/></linearGradient><path d="M0 1.564l.007.001V.007L0 .002v1.562z" id="d"/><linearGradient x1="0%" y1="50.019%" x2="1.21%" y2="50.019%" id="e"><stop stop-color="#313477" offset="0%"/><stop stop-color="#0077BC" offset="100%"/></linearGradient><path d="M3.976.575C2.088.575.363 2.278.363 4.188v4.945c1.132-.885 2.958-1.319 5.281-1.14 1.322.102 2.3.286 2.834.445v1.57c-.588-.294-1.748-.73-2.715-.8-2.191-.158-3.342.868-3.342 2.528 0 1.494.888 2.773 3.331 2.602.806-.056 2.148-.525 2.719-.797l.007 1.523c-.492.155-2.02.488-3.458.5-2.165.017-3.694-.443-4.659-1.189L.36 22.941h5.643c1.54 0 3.546-1.439 3.546-3.627V.575H3.976z" id="g"/><linearGradient x1=".004%" y1="49.999%" x2="99.996%" y2="49.999%" id="h"><stop stop-color="#753136" offset="0%"/><stop stop-color="#ED1746" offset="100%"/></linearGradient><path d="M.123.448L.119 2.424l2.21.007c.43 0 .97-.368.97-1.01a.97.97 0 0 0-.967-.973c-.308.003-.8.001-1.245 0L.375.446C.26.446.17.446.123.448" id="j"/><linearGradient x1="0%" y1="50.008%" x2="99.996%" y2="50.008%" id="k"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient><path d="M.115.473l-.008 1.8 2.089.014c.346-.007.834-.325.834-.882 0-.567-.426-.95-.88-.939-.296.008-.702.005-1.078.002L.52.465C.333.465.187.467.115.473" id="m"/><linearGradient x1=".022%" y1="49.994%" x2="100.012%" y2="49.994%" id="n"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient><path d="M3.694.575C1.806.575.08 2.278.08 4.188L.08 8.164h5.365c1.067 0 2.324.457 2.324 1.754 0 .696-.37 1.485-1.706 1.74v.03c.78 0 2.132.457 2.132 1.833 0 1.423-1.46 1.817-2.243 1.817l-5.873.006-.001 7.597H5.72c1.54 0 3.544-1.439 3.544-3.627V.575H3.694z" id="p"/><linearGradient x1="-.007%" y1="49.999%" x2="100.004%" y2="49.999%" id="q"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.013)"><mask id="c" fill="#fff"><use href="#a"/></mask><path d="M3.622.575C1.734.575.009 2.278.009 4.188c0 1.051 0 5.212-.002 9.348.346.217 2.01.752 2.68.799 1.466.103 2.375-.381 2.515-1.603l-.007-4.538h3.243v4.434c-.17 2.54-2.399 3.057-5.79 2.887-.877-.046-2.07-.27-2.64-.42L.004 22.94H5.65c1.54 0 3.544-1.439 3.544-3.627V.575H3.622z" fill="url(#b)" mask="url(#c)"/></g><g transform="translate(0 16.543)"><mask id="f" fill="#fff"><use href="#d"/></mask><path d="M0 1.564l.007.001V.007L0 .002v1.562z" fill="url(#e)" mask="url(#f)"/></g><g transform="translate(10 3.013)"><mask id="i" fill="#fff"><use href="#g"/></mask><path d="M3.976.575C2.088.575.363 2.278.363 4.188v4.945c1.132-.885 2.958-1.319 5.281-1.14 1.322.102 2.3.286 2.834.445v1.57c-.588-.294-1.748-.73-2.715-.8-2.191-.158-3.342.868-3.342 2.528 0 1.494.888 2.773 3.331 2.602.806-.056 2.148-.525 2.719-.797l.007 1.523c-.492.155-2.02.488-3.458.5-2.165.017-3.694-.443-4.659-1.189L.36 22.941h5.643c1.54 0 3.546-1.439 3.546-3.627V.575H3.976z" fill="url(#h)" mask="url(#i)"/></g><g transform="translate(22.353 14.778)"><mask id="l" fill="#fff"><use href="#j"/></mask><path d="M.123.448L.119 2.424l2.21.007c.43 0 .97-.368.97-1.01a.97.97 0 0 0-.967-.973c-.308.003-.8.001-1.245 0L.375.446C.26.446.17.446.123.448" fill="url(#k)" mask="url(#l)"/></g><g transform="translate(22.353 11.837)"><mask id="o" fill="#fff"><use href="#m"/></mask><path d="M.115.473l-.008 1.8 2.089.014c.346-.007.834-.325.834-.882 0-.567-.426-.95-.88-.939-.296.008-.702.005-1.078.002L.52.465C.333.465.187.467.115.473" fill="url(#n)" mask="url(#o)"/></g><g transform="translate(20.588 3.013)"><mask id="r" fill="#fff"><use href="#p"/></mask><path d="M3.694.575C1.806.575.08 2.278.08 4.188L.08 8.164h5.365c1.067 0 2.324.457 2.324 1.754 0 .696-.37 1.485-1.706 1.74v.03c.78 0 2.132.457 2.132 1.833 0 1.423-1.46 1.817-2.243 1.817l-5.873.006-.001 7.597H5.72c1.54 0 3.544-1.439 3.544-3.627V.575H3.694z" fill="url(#q)" mask="url(#r)"/></g></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg b/browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg
new file mode 100644
index 0000000000..3e0f21f9e3
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg
@@ -0,0 +1 @@
+<svg width="38" height="30" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M7.485 29.258v-1.896a1.125 1.125 0 0 0-1.188-1.2 1.17 1.17 0 0 0-1.061.537 1.109 1.109 0 0 0-.999-.537.998.998 0 0 0-.885.448v-.373h-.657v3.021h.664v-1.662a.708.708 0 0 1 .74-.802c.435 0 .656.284.656.796v1.68h.664v-1.674a.71.71 0 0 1 .74-.802c.448 0 .663.284.663.796v1.68l.663-.012zm9.817-3.02h-1.08v-.917h-.664v.916h-.6v.6h.613v1.391c0 .701.271 1.119 1.049 1.119.29 0 .575-.08.821-.234l-.19-.563a1.213 1.213 0 0 1-.58.17c-.317 0-.437-.201-.437-.505v-1.377h1.074l-.006-.6zm5.605-.076a.891.891 0 0 0-.796.442v-.367h-.65v3.021h.656v-1.693c0-.5.215-.778.632-.778.14-.002.28.024.411.076l.202-.632a1.406 1.406 0 0 0-.467-.082l.012.013zm-8.474.316a2.26 2.26 0 0 0-1.232-.316c-.765 0-1.264.366-1.264.966 0 .493.367.797 1.043.891l.316.045c.36.05.53.145.53.316 0 .234-.24.366-.688.366-.361.01-.715-.1-1.005-.316l-.316.512a2.18 2.18 0 0 0 1.308.392c.872 0 1.378-.41 1.378-.986 0-.575-.398-.809-1.056-.904l-.316-.044c-.284-.038-.511-.095-.511-.297 0-.202.214-.354.575-.354.333.004.659.093.947.26l.291-.531zm17.602-.316a.891.891 0 0 0-.796.442v-.367h-.65v3.021h.656v-1.693c0-.5.215-.778.632-.778.14-.002.28.024.411.076l.202-.632a1.406 1.406 0 0 0-.467-.082l.012.013zm-8.467 1.58a1.526 1.526 0 0 0 1.611 1.58 1.58 1.58 0 0 0 1.087-.36l-.316-.532a1.327 1.327 0 0 1-.79.272.97.97 0 0 1 0-1.934c.286.003.563.099.79.272l.316-.53a1.58 1.58 0 0 0-1.087-.361 1.526 1.526 0 0 0-1.611 1.58v.012zm6.155 0v-1.505h-.658v.367a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 0 0 0 3.16c.37.013.722-.152.948-.443v.366h.658v-1.504zm-2.446 0a.913.913 0 1 1 .916.966.907.907 0 0 1-.916-.967zm-7.93-1.58a1.58 1.58 0 1 0 .044 3.16c.454.023.901-.124 1.254-.411l-.316-.487c-.25.2-.559.311-.878.316a.837.837 0 0 1-.904-.74h2.243v-.252c0-.948-.587-1.58-1.434-1.58l-.01-.006zm0 .587a.749.749 0 0 1 .764.733h-1.58a.777.777 0 0 1 .803-.733h.012zm16.464.999v-2.724h-.632v1.58a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 0 0 0 3.16c.369.013.722-.152.948-.443v.366h.632v-1.497zm1.096 1.07a.316.316 0 0 1 .218.086.294.294 0 0 1-.098.487.297.297 0 0 1-.12.025.316.316 0 0 1-.284-.183.297.297 0 0 1 .066-.329.316.316 0 0 1 .228-.085h-.01zm0 .535a.224.224 0 0 0 .165-.07.234.234 0 0 0 0-.316.234.234 0 0 0-.165-.07.237.237 0 0 0-.167.07.234.234 0 0 0 0 .316.234.234 0 0 0 .076.05c.032.015.066.021.101.02h-.01zm.02-.376a.126.126 0 0 1 .082.025c.02.016.03.041.028.066a.076.076 0 0 1-.022.057.11.11 0 0 1-.066.029l.091.104h-.072l-.086-.104h-.028v.104h-.06v-.278l.132-.003zm-.07.054v.075h.07a.066.066 0 0 0 .037 0 .032.032 0 0 0 0-.028.032.032 0 0 0 0-.028.066.066 0 0 0-.038 0l-.07-.02zm-3.476-1.283a.913.913 0 1 1 .917.967.907.907 0 0 1-.917-.967zm-22.19 0v-1.51h-.657v.366a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 1 0 0 3.16c.369.013.722-.152.948-.443v.366h.657v-1.497zm-2.445 0a.913.913 0 1 1 .916.967.907.907 0 0 1-.922-.967h.006z" fill="#231F20"/><path fill="#FF5F00" d="M14.215 3.22h9.953v17.886h-9.953z"/><path d="M14.847 12.165a11.356 11.356 0 0 1 4.345-8.945 11.375 11.375 0 1 0 0 17.886 11.356 11.356 0 0 1-4.345-8.941z" fill="#EB001B"/><path d="M37.596 12.165a11.375 11.375 0 0 1-18.404 8.941 11.375 11.375 0 0 0 0-17.886 11.375 11.375 0 0 1 18.404 8.941v.004zM36.51 19.265v-.412h.148v-.085h-.376v.085h.161v.412h.066zm.73 0v-.497h-.115l-.132.355-.133-.355h-.101v.497h.082v-.373l.123.323h.086l.123-.323v.376l.066-.003z" fill="#F79E1B"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-mir.svg b/browser/extensions/formautofill/content/third-party/cc-logo-mir.svg
new file mode 100644
index 0000000000..26a24f985d
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-mir.svg
@@ -0,0 +1 @@
+<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="100%" y1="312.751%" x2=".612%" y2="312.751%" id="a"><stop stop-color="#1E5CD8" offset="0%"/><stop stop-color="#02AFFF" offset="100%"/></linearGradient></defs><g fill-rule="nonzero" fill="none"><path d="M7.812 11.313l-1.326 4.593h-.227l-1.326-4.594A1.823 1.823 0 0 0 3.18 10H0v10h3.184v-5.91h.227L5.234 20H7.51l1.819-5.91h.226V20h3.185V10H9.56c-.81 0-1.522.535-1.75 1.313zM25.442 20h3.204v-2.957h3.223c1.686 0 3.122-.953 3.677-2.293H25.442V20zm-5.676-8.945l-2.241 4.855h-.227V10h-3.184v10h2.703c.712 0 1.357-.414 1.654-1.055l2.242-4.851h.227V20h3.184V10H21.42c-.712 0-1.358.414-1.655 1.055z" fill="#006848"/><path d="M32.186 0c.92 0 1.752.352 2.382.93a3.49 3.49 0 0 1 1.146 2.59c0 .21-.023.417-.058.62H29.74a4.478 4.478 0 0 1-4.272-3.124c-.007-.02-.011-.043-.02-.067-.015-.054-.027-.113-.042-.168A4.642 4.642 0 0 1 25.293 0h6.893z" fill="url(#a)" transform="translate(0 10)"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg b/browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg
new file mode 100644
index 0000000000..99ef7e86b4
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg
@@ -0,0 +1 @@
+<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><defs><path id="a" d="M0 .04h17.771v22.433H0z"/><path id="c" d="M.134.04h18.093v22.433H.134z"/><path id="e" d="M.202.04h17.77v22.433H.202z"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.179)"><mask id="b" fill="#fff"><use href="#a"/></mask><path d="M7.023.04h8.952C17.225.04 18 1.057 17.71 2.31l-4.168 17.893c-.294 1.25-1.545 2.269-2.795 2.269h-8.95c-1.248 0-2.027-1.02-1.736-2.269l4.17-17.893C4.52 1.058 5.771.04 7.022.04" fill="#E21837" mask="url(#b)"/></g><g transform="translate(8.073 3.179)"><mask id="d" fill="#fff"><use href="#c"/></mask><path d="M7.157.04h10.294c1.25 0 .686 1.018.392 2.271l-4.167 17.893c-.292 1.25-.201 2.269-1.453 2.269H1.93c-1.252 0-2.026-1.02-1.732-2.269L4.363 2.311C4.66 1.058 5.907.04 7.157.04" fill="#00457C" mask="url(#d)"/></g><g transform="translate(17.89 3.179)"><mask id="f" fill="#fff"><use href="#e"/></mask><path d="M7.224.04h8.952c1.251 0 2.028 1.018 1.734 2.271l-4.166 17.893c-.295 1.25-1.547 2.269-2.798 2.269H2c-1.252 0-2.028-1.02-1.735-2.269L4.432 2.311C4.723 1.058 5.972.04 7.224.04" fill="#007B84" mask="url(#f)"/></g><path d="M26.582 16.428L25.49 20.04h.295l-.228.746h-.292l-.069.23h-1.038l.07-.23H22.12l.21-.69h.215l1.106-3.667.22-.739h1.06l-.111.373s.282-.203.55-.272c.266-.07 1.801-.096 1.801-.096l-.227.734h-.362zm-1.866 0l-.28.923s.315-.142.484-.189c.174-.046.434-.061.434-.061l.203-.673h-.841zm-.42 1.38l-.29.96s.321-.163.492-.215c.174-.039.438-.072.438-.072l.205-.673h-.845zm-.675 2.24h.844l.242-.81h-.841l-.245.81z" fill="#FEFEFE"/><path d="M27.05 15.694h1.13l.012.42c-.008.072.054.106.186.106h.23l-.21.695h-.612c-.528.038-.73-.19-.715-.445l-.022-.776zM27.2 18.993H26.12l.185-.619h1.232l.175-.566h-1.216l.207-.698h3.384l-.21.698h-1.135l-.178.566h1.139l-.19.619h-1.229l-.219.26h.5l.121.78c.014.078.014.13.04.162.025.028.175.042.262.042h.152l-.231.759h-.385c-.058 0-.147-.005-.27-.01-.114-.01-.195-.077-.273-.116a.367.367 0 0 1-.202-.265l-.12-.778-.56.766c-.177.243-.417.428-.824.428h-.782l.205-.677h.3a.484.484 0 0 0 .218-.063.336.336 0 0 0 .166-.138l.816-1.15zM15.397 17.298h2.855l-.211.68H16.9l-.179.581h1.168l-.213.702h-1.167l-.284.945c-.034.104.278.117.39.117l.584-.08-.235.778H15.65c-.106 0-.185-.015-.299-.04a.312.312 0 0 1-.209-.153c-.048-.077-.122-.14-.071-.305l.378-1.25H14.8l.215-.714h.65l.173-.581h-.648l.207-.68zM17.317 16.074h1.171l-.212.712h-1.6l-.173.15c-.075.072-.1.042-.198.094-.09.045-.28.136-.525.136h-.513l.207-.684h.154c.13 0 .219-.012.264-.04a.617.617 0 0 0 .171-.222l.296-.535h1.163l-.205.389zM18.991 15.694h.997l-.146.502s.316-.252.536-.343c.22-.081.716-.154.716-.154l1.615-.01-.55 1.832a2.139 2.139 0 0 1-.269.608.7.7 0 0 1-.271.251 1.02 1.02 0 0 1-.375.126c-.106.008-.27.01-.496.014h-1.556l-.437 1.447c-.042.144-.061.213-.034.252a.18.18 0 0 0 .148.073l.686-.065-.235.794h-.766c-.245 0-.422-.006-.547-.015-.118-.01-.242 0-.325-.063-.07-.063-.18-.147-.177-.231.007-.078.04-.209.09-.389l1.396-4.63zm2.117 1.848h-1.634l-.1.33h1.414c.167-.02.202.004.216-.004l.104-.326zm-1.545-.297s.32-.292.867-.387c.124-.023.9-.015.9-.015l.119-.392h-1.647l-.24.794z" fill="#FEFEFE"/><path d="M21.899 18.648l-.093.44c-.04.137-.073.24-.177.328-.11.093-.237.19-.536.19l-.554.023-.005.497c-.005.14.032.126.054.149.026.025.049.035.073.045l.175-.01.529-.03-.22.726h-.606c-.425 0-.74-.01-.842-.091-.103-.065-.116-.146-.115-.286l.04-1.938h.968l-.014.397h.233c.08 0 .134-.008.167-.03a.175.175 0 0 0 .065-.1l.097-.31h.76zM8.082 8.932c-.033.158-.655 3.024-.656 3.026-.134.58-.231.993-.562 1.26a1 1 0 0 1-.66.23c-.409 0-.646-.203-.687-.587l-.007-.132.124-.781s.652-2.611.769-2.957l.01-.039c-1.27.011-1.495 0-1.51-.02-.009.028-.04.19-.04.19l-.666 2.943-.057.25-.11.816c0 .242.047.44.142.607.303.53 1.168.609 1.657.609.63 0 1.222-.134 1.622-.378.694-.41.875-1.051 1.037-1.62l.075-.293s.672-2.712.786-3.065c.004-.02.006-.03.012-.039-.92.01-1.192 0-1.28-.02M11.798 14.319c-.45-.008-.61-.008-1.135.02l-.02-.04c.045-.2.095-.398.14-.6l.065-.275c.097-.425.191-.92.202-1.072.01-.09.042-.317-.218-.317-.109 0-.223.053-.339.107-.063.226-.19.863-.252 1.153-.13.61-.138.681-.197.983l-.038.041a12.946 12.946 0 0 0-1.159.02l-.024-.046c.089-.362.178-.728.263-1.091.224-.986.278-1.362.338-1.863l.044-.03c.52-.073.647-.088 1.21-.202l.048.053-.087.313c.096-.057.187-.114.283-.163.266-.13.562-.17.724-.17.248 0 .518.069.63.355.107.254.036.567-.104 1.184l-.072.316c-.144.686-.168.812-.25 1.283l-.052.041zM13.627 14.319c-.272-.002-.448-.008-.617-.002-.17.002-.335.01-.588.022l-.013-.022-.016-.024c.069-.26.106-.35.14-.443a3.13 3.13 0 0 0 .128-.449c.08-.345.128-.586.16-.797.037-.204.057-.378.085-.58l.02-.015.02-.02c.27-.037.442-.062.618-.09.177-.023.355-.06.635-.113l.01.024.008.025c-.052.214-.105.427-.156.643-.05.217-.103.43-.15.643-.101.453-.142.623-.166.745-.024.115-.03.178-.069.412l-.025.021-.024.02zM17.67 12.768c.159-.692.036-1.015-.119-1.212-.234-.3-.648-.396-1.078-.396-.258 0-.873.025-1.354.468-.345.32-.505.754-.6 1.17-.098.423-.21 1.186.492 1.47.216.093.528.118.73.118.513 0 1.04-.141 1.436-.561.305-.341.445-.848.494-1.057m-1.18-.05c-.022.117-.124.551-.262.736-.097.136-.21.219-.337.219-.037 0-.26 0-.264-.332-.002-.163.031-.33.072-.512.119-.524.258-.964.616-.964.28 0 .3.328.175.853M28.677 14.365c-.544-.004-.7-.004-1.202.017l-.031-.04c.135-.517.272-1.032.393-1.554.158-.678.194-.966.245-1.363l.041-.033c.54-.077.69-.099 1.252-.203l.016.047c-.103.426-.203.85-.304 1.278-.206.893-.281 1.346-.36 1.813l-.05.038z" fill="#FEFEFE"/><path d="M28.935 12.83c.158-.688-.479-.062-.58-.289-.154-.354-.058-1.072-.683-1.312-.24-.095-.804.027-1.29.469-.34.315-.504.747-.597 1.161-.098.418-.21 1.18.488 1.452.222.095.422.123.624.113.702-.038 1.236-1.098 1.633-1.516.305-.333.358.124.405-.079m-1.074-.05c-.027.112-.13.549-.268.732-.092.13-.311.211-.437.211-.036 0-.257 0-.264-.325a2.225 2.225 0 0 1 .073-.512c.12-.515.258-.95.616-.95.28 0 .4.316.28.843M20.746 14.319a12.427 12.427 0 0 0-1.134.02l-.02-.04c.046-.2.097-.398.144-.6l.061-.275c.099-.425.194-.92.203-1.072.01-.09.042-.317-.216-.317-.113 0-.225.053-.341.107-.062.226-.192.863-.255 1.153-.126.61-.136.681-.193.983l-.04.041a12.904 12.904 0 0 0-1.156.02l-.024-.046c.088-.362.177-.728.262-1.091.224-.986.276-1.362.339-1.863l.04-.03c.52-.073.648-.088 1.212-.202l.043.053-.08.313a4.81 4.81 0 0 1 .281-.163c.264-.13.562-.17.724-.17.244 0 .516.069.632.355.105.254.033.567-.108 1.184l-.07.316c-.15.686-.17.812-.25 1.283l-.054.041zM25.133 10.61c-.079.359-.312.66-.61.806-.247.124-.549.134-.86.134h-.201l.015-.08.37-1.608.011-.082.005-.063.149.015.782.067c.302.117.426.418.34.81m-.487-1.68l-.375.003c-.974.012-1.364.008-1.524-.011l-.04.197-.348 1.618-.874 3.597c.85-.01 1.199-.01 1.345.006.034-.161.23-1.121.232-1.121 0 0 .168-.704.178-.73 0 0 .053-.073.106-.102h.078c.732 0 1.56 0 2.209-.477.441-.328.743-.81.877-1.398.035-.144.061-.315.061-.487 0-.225-.045-.447-.176-.62-.33-.464-.99-.472-1.75-.476M33.124 11.185l-.043-.05c-.556.113-.656.131-1.167.2l-.038.038-.005.024-.002-.009c-.38.877-.37.688-.679 1.378l-.003-.084-.077-1.497-.05-.05c-.581.113-.595.131-1.133.2l-.041.038c-.006.017-.006.037-.01.059l.004.007c.067.344.05.267.118.809.032.266.073.533.105.796.053.44.083.656.147 1.327-.363.6-.449.826-.798 1.352l.022.049c.524-.02.646-.02 1.035-.02l.084-.096c.294-.633 2.531-4.47 2.531-4.47M14.12 11.556c.298-.207.335-.493.085-.641-.254-.15-.7-.102-1 .105-.3.203-.333.49-.08.642.25.146.697.103.994-.106" fill="#FEFEFE"/><path d="M30.554 15.709l-.437.75c-.139.256-.395.447-.803.448l-.696-.012.203-.674h.137c.07 0 .121-.003.16-.023.036-.012.062-.04.09-.08l.258-.409h1.088z" fill="#FEFEFE"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-visa.svg b/browser/extensions/formautofill/content/third-party/cc-logo-visa.svg
new file mode 100644
index 0000000000..57bcc144d1
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-visa.svg
@@ -0,0 +1 @@
+<svg width="44" height="30" xmlns="http://www.w3.org/2000/svg"><defs><path d="M22.8 9.786c-.025-1.96 1.765-3.053 3.113-3.703 1.385-.667 1.85-1.095 1.845-1.691-.01-.913-1.105-1.316-2.13-1.332-1.787-.027-2.826.478-3.652.86L21.332.938c.83-.378 2.364-.708 3.956-.722 3.735 0 6.18 1.824 6.193 4.653.014 3.59-5.02 3.79-4.985 5.395.012.486.481 1.005 1.51 1.138.508.066 1.914.117 3.506-.609l.626 2.884a9.623 9.623 0 0 1-3.329.605c-3.516 0-5.99-1.85-6.01-4.497m15.347 4.248a1.621 1.621 0 0 1-1.514-.998L31.296.428h3.733l.743 2.032h4.561l.431-2.032h3.29l-2.87 13.606h-3.038m.522-3.675l1.077-5.11h-2.95l1.873 5.11m-20.394 3.675L15.33.428h3.557l2.942 13.606h-3.556m-8.965-9.26L7.81 12.648c-.176.879-.87 1.386-1.64 1.386H.116l-.084-.395c1.242-.267 2.654-.697 3.51-1.157.523-.282.672-.527.844-1.196L7.224.428h3.76l5.763 13.606H13.01L9.31 4.774z" id="a"/><linearGradient x1="16.148%" y1="34.401%" x2="85.832%" y2="66.349%" id="b"><stop stop-color="#222357" offset="0%"/><stop stop-color="#254AA5" offset="100%"/></linearGradient></defs><g transform="matrix(1 0 0 -1 0 22.674)" fill="none" fill-rule="evenodd"><mask id="c" fill="#fff"><use href="#a"/></mask><path fill="url(#b)" fill-rule="nonzero" mask="url(#c)" d="M-4.669 12.849l44.237 16.12L49.63 1.929 5.395-14.19"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/docs/heuristics.rst b/browser/extensions/formautofill/docs/heuristics.rst
new file mode 100644
index 0000000000..cf6e49da39
--- /dev/null
+++ b/browser/extensions/formautofill/docs/heuristics.rst
@@ -0,0 +1,36 @@
+Form Autofill Heuristics
+========================
+
+Form Autofill Heuristics module is for detecting the field type based on `autocomplete attribute <https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill>`_, `the regular expressions <http://searchfox.org/mozilla-central/source/browser/extensions/formautofill/content/heuristicsRegexp.js>`_ and the customized logic in each parser.
+
+Debugging
+---------
+
+The pref ``extensions.formautofill.heuristics.enabled`` is "true" in default. Set it to "false" could be useful to verify the result of autocomplete attribute.
+
+Dependent APIs
+--------------
+
+``element.getAutocompleteInfo()`` provides the parsed result of ``autocomplete`` attribute which includes the field name and section information defined in `autofill spec <https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill>`_
+
+Regular Expressions
+-------------------
+
+This section is about how the regular expression is applied during parsing fields. All regular expressions are in `heuristicsRegexp.js <https://searchfox.org/mozilla-central/source/browser/extensions/formautofill/content/heuristicsRegexp.js>`_.
+
+Parser Implementations
+----------------------
+
+The parsers are for detecting the field type more accurately based on the near context of a field. Each parser uses ``FieldScanner`` to traverse the interested fields with the result from the regular expressions and adjust each field type when it matches to a grammar.
+
+* _parsePhoneFields
+
+ * related type: ``tel``, ``tel-*``
+
+* _parseAddressFields
+
+ * related type: ``address-line[1-3]``
+
+* _parseCreditCardExpirationDateFields
+
+ * related type: ``cc-exp``, ``cc-exp-month``, ``cc-exp-year``
diff --git a/browser/extensions/formautofill/docs/index.rst b/browser/extensions/formautofill/docs/index.rst
new file mode 100644
index 0000000000..37759ce602
--- /dev/null
+++ b/browser/extensions/formautofill/docs/index.rst
@@ -0,0 +1,30 @@
+Form Autofill
+=============
+
+`Wiki <https://wiki.mozilla.org/Firefox/Features/Form_Autofill>`_ |
+`IRC: #formfill <https://chat.mozilla.org/#/room/#form-autofill:mozilla.org>`_
+
+Introduction
+------------
+
+Form Autofill saves users time and effort when making online purchases by storing their personal information in a profile and automatically populating form fields when the user requires it.
+
+Our objective is to increase user engagement, satisfaction and retention for frequent online shoppers (those who make an online purchase at least once per month). We believe this can be achieved by enabling users to complete forms and “check out” in e-commerce flows as quickly and securely as possible.
+
+Debugging
+---------
+
+Set the pref ``extensions.formautofill.loglevel`` to "Debug".
+
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ heuristics
+
+Report Issues
+-------------
+
+If you find any issues about filling a form with incorrect values, please file a `new bug <https://bugzilla.mozilla.org/enter_bug.cgi?product=Toolkit&component=Form%20Autofill>`_ to Toolkit::Form Autofill component or leave a comment in `bug 1405266 <https://bugzilla.mozilla.org/show_bug.cgi?id=1405266>`_.
diff --git a/browser/extensions/formautofill/jar.mn b/browser/extensions/formautofill/jar.mn
new file mode 100644
index 0000000000..4253184cf5
--- /dev/null
+++ b/browser/extensions/formautofill/jar.mn
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[features/formautofill@mozilla.org] chrome.jar:
+ content/ (content/*)
+ content/skin/ (skin/shared/*)
diff --git a/browser/extensions/formautofill/locales/en-US/formautofill.properties b/browser/extensions/formautofill/locales/en-US/formautofill.properties
new file mode 100644
index 0000000000..0a43a3c09b
--- /dev/null
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -0,0 +1,127 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (autofillOptionsLink, autofillOptionsLinkOSX): These strings are used in the doorhanger for
+# updating addresses. The link leads users to Form Autofill browser preferences.
+autofillOptionsLink = Form Autofill Options
+autofillOptionsLinkOSX = Form Autofill Preferences
+# LOCALIZATION NOTE (changeAutofillOptions, changeAutofillOptionsOSX): These strings are used on the doorhanger
+# that notifies users that addresses are saved. The button leads users to Form Autofill browser preferences.
+changeAutofillOptions = Change Form Autofill Options
+changeAutofillOptionsOSX = Change Form Autofill Preferences
+changeAutofillOptionsAccessKey = C
+# LOCALIZATION NOTE (addressesSyncCheckbox): If Sync is enabled, this checkbox is displayed on the doorhanger
+# shown when saving addresses.
+addressesSyncCheckbox = Share addresses with synced devices
+# LOCALIZATION NOTE (creditCardsSyncCheckbox): If Sync is enabled and credit card sync is available,
+# this checkbox is displayed on the doorhanger shown when saving credit card.
+creditCardsSyncCheckbox = Share credit cards with synced devices
+
+# LOCALIZATION NOTE (saveAddressesMessage): %S is brandShortName. This string is used on the doorhanger to
+# notify users that addresses are saved.
+saveAddressesMessage = %S now saves addresses so you can fill out forms faster.
+saveAddressDescriptionLabel = Address to save:
+saveAddressLabel = Save Address
+saveAddressAccessKey = S
+# LOCALIZATION NOTE (updateAddressMessage, updateAddressDescriptionLabel, createAddressLabel, updateAddressLabel):
+# Used on the doorhanger when an address change is detected.
+updateAddressMessage = Would you like to update your address with this new information?
+updateAddressOldDescriptionLabel = Old Address:
+updateAddressNewDescriptionLabel = New Address:
+createAddressLabel = Create New Address
+createAddressAccessKey = C
+createAddressDescriptionLabel = Address to create:
+cancelAddressLabel = Don’t Save
+cancelAddressAccessKey = D
+updateAddressLabel = Update Address
+updateAddressAccessKey = U
+# LOCALIZATION NOTE (saveCreditCardMessage, saveCreditCardDescriptionLabel, saveCreditCardLabel, cancelCreditCardLabel, neverSaveCreditCardLabel):
+# Used on the doorhanger when users submit payment with credit card.
+# LOCALIZATION NOTE (saveCreditCardMessage): %S is brandShortName.
+saveCreditCardMessage = Would you like %S to save this credit card? (Security code will not be saved)
+saveCreditCardDescriptionLabel = Credit card to save:
+saveCreditCardLabel = Save Credit Card
+saveCreditCardAccessKey = S
+cancelCreditCardLabel = Don’t Save
+cancelCreditCardAccessKey = D
+neverSaveCreditCardLabel = Never Save Credit Cards
+neverSaveCreditCardAccessKey = N
+# LOCALIZATION NOTE (updateCreditCardMessage, updateCreditCardDescriptionLabel, createCreditCardLabel, updateCreditCardLabel):
+# Used on the doorhanger when an credit card change is detected.
+updateCreditCardMessage = Would you like to update your credit card with this new information?
+updateCreditCardDescriptionLabel = Credit card to update:
+createCreditCardLabel = Create New Credit Card
+createCreditCardAccessKey = C
+updateCreditCardLabel = Update Credit Card
+updateCreditCardAccessKey = U
+# LOCALIZATION NOTE (openAutofillMessagePanel): Tooltip label for Form Autofill doorhanger icon on address bar.
+openAutofillMessagePanel = Open Form Autofill message panel
+
+# LOCALIZATION NOTE (autocompleteFooterOption2):
+# Used as a label for the button, displayed at the bottom of the dropdown suggestion, to open Form Autofill browser preferences.
+autocompleteFooterOption2 = Form Autofill Options
+# LOCALIZATION NOTE (autocompleteFooterOptionOSX2):
+# Used as a label for the button, displayed at the bottom of the dropdown suggestion, to open Form Autofill browser preferences.
+autocompleteFooterOptionOSX2 = Form Autofill Preferences
+# LOCALIZATION NOTE (autocompleteFooterOptionShort2):
+# Used as a label for the button, displayed at the bottom of the dropdown suggestion, to open Form Autofill browser preferences.
+# The short version is used for inputs below a certain width (e.g. 150px).
+autocompleteFooterOptionShort2 = Autofill Options
+# LOCALIZATION NOTE (autocompleteFooterOptionOSXShort2):
+# Used as a label for the button, displayed at the bottom of the dropdown suggestion, to open Form Autofill browser preferences.
+# The short version is used for inputs below a certain width (e.g. 150px).
+autocompleteFooterOptionOSXShort2 = Autofill Preferences
+# LOCALIZATION NOTE (category.address, category.name, category.organization2, category.tel, category.email):
+# Used in autofill drop down suggestion to indicate what other categories Form Autofill will attempt to fill.
+category.address = address
+category.name = name
+category.organization2 = organization
+category.tel = phone
+category.email = email
+# LOCALIZATION NOTE (fieldNameSeparator): This is used as a separator between categories.
+fieldNameSeparator = ,\u0020
+# LOCALIZATION NOTE (phishingWarningMessage, phishingWarningMessage2): The warning
+# text that is displayed for informing users what categories are about to be filled.
+# "%S" will be replaced with a list generated from the pre-defined categories.
+# The text would be e.g. Also autofills organization, phone, email.
+phishingWarningMessage = Also autofills %S
+phishingWarningMessage2 = Autofills %S
+# LOCALIZATION NOTE (insecureFieldWarningDescription): %S is brandShortName. This string is used in drop down
+# suggestion when users try to autofill credit card on an insecure website (without https).
+insecureFieldWarningDescription = %S has detected an insecure site. Form Autofill is temporarily disabled.
+# LOCALIZATION NOTE (clearFormBtnLabel2): Label for the button in the dropdown menu that used to clear the populated
+# form.
+clearFormBtnLabel2 = Clear Autofill Form
+
+autofillHeader = Forms and Autofill
+# LOCALIZATION NOTE (autofillAddressesCheckbox): Label for the checkbox that enables autofilling addresses.
+autofillAddressesCheckbox = Autofill addresses
+# LOCALIZATION NOTE (learnMoreLabel): Label for the link that leads users to the Form Autofill SUMO page.
+learnMoreLabel = Learn more
+# LOCALIZATION NOTE (savedAddressesBtnLabel): Label for the button that opens a dialog that shows the
+# list of saved addresses.
+savedAddressesBtnLabel = Saved Addresses…
+# LOCALIZATION NOTE (autofillCreditCardsCheckbox): Label for the checkbox that enables autofilling credit cards.
+autofillCreditCardsCheckbox = Autofill credit cards
+# LOCALIZATION NOTE (savedCreditCardsBtnLabel): Label for the button that opens a dialog that shows the list
+# of saved credit cards.
+savedCreditCardsBtnLabel = Saved Credit Cards…
+
+autofillReauthCheckboxMac = Require macOS authentication to autofill, view, or edit stored credit cards.
+autofillReauthCheckboxWin = Require Windows authentication to autofill, view, or edit stored credit cards.
+autofillReauthCheckboxLin = Require Linux authentication to autofill, view, or edit stored credit cards.
+
+# LOCALIZATION NOTE (autofillReauthOSDialogMac): This string is
+# preceded by the operating system (macOS) with "Firefox is trying to ", and
+# has a period added to its end. Make sure to test in your locale.
+autofillReauthOSDialogMac = change the authentication settings
+autofillReauthOSDialogWin = To change the authentication settings, enter your Windows login credentials.
+autofillReauthOSDialogLin = To change the authentication settings, enter your Linux login credentials.
+
+useCreditCardPasswordPrompt.win = %S is trying to use stored credit card information. Confirm access to this Windows account below.
+# LOCALIZATION NOTE (useCreditCardPasswordPrompt.macos): This string is
+# preceded by the operating system (macOS) with "Firefox is trying to ", and
+# has a period added to its end. Make sure to test in your locale.
+useCreditCardPasswordPrompt.macos = use stored credit card information
+useCreditCardPasswordPrompt.linux = %S is trying to use stored credit card information.
diff --git a/browser/extensions/formautofill/locales/jar.mn b/browser/extensions/formautofill/locales/jar.mn
new file mode 100644
index 0000000000..58c364e55f
--- /dev/null
+++ b/browser/extensions/formautofill/locales/jar.mn
@@ -0,0 +1,8 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[features/formautofill@mozilla.org] @AB_CD@.jar:
+% locale formautofill @AB_CD@ %locale/@AB_CD@/
+ locale/@AB_CD@/formautofill.properties (%formautofill.properties)
diff --git a/browser/extensions/formautofill/locales/moz.build b/browser/extensions/formautofill/locales/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/browser/extensions/formautofill/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/extensions/formautofill/manifest.json b/browser/extensions/formautofill/manifest.json
new file mode 100644
index 0000000000..f79a3fdb76
--- /dev/null
+++ b/browser/extensions/formautofill/manifest.json
@@ -0,0 +1,26 @@
+{
+ "manifest_version": 2,
+ "name": "Form Autofill",
+ "version": "1.0.1",
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "formautofill@mozilla.org"
+ }
+ },
+
+ "background": {
+ "scripts": ["background.js"]
+ },
+
+ "experiment_apis": {
+ "formautofill": {
+ "schema": "schema.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "api.js",
+ "events": ["startup"]
+ }
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/moz.build b/browser/extensions/formautofill/moz.build
new file mode 100644
index 0000000000..272319b131
--- /dev/null
+++ b/browser/extensions/formautofill/moz.build
@@ -0,0 +1,58 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"]
+DEFINES["MOZ_APP_MAXVERSION"] = CONFIG["MOZ_APP_MAXVERSION"]
+
+DIRS += ["locales"]
+
+FINAL_TARGET_FILES.features["formautofill@mozilla.org"] += [
+ "api.js",
+ "background.js",
+ "manifest.json",
+ "schema.json",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ FINAL_TARGET_FILES.features["formautofill@mozilla.org"].chrome.content.skin += [
+ "skin/linux/autocomplete-item.css",
+ "skin/linux/editDialog.css",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ FINAL_TARGET_FILES.features["formautofill@mozilla.org"].chrome.content.skin += [
+ "skin/osx/autocomplete-item.css",
+ "skin/osx/editDialog.css",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ FINAL_TARGET_FILES.features["formautofill@mozilla.org"].chrome.content.skin += [
+ "skin/windows/autocomplete-item.css",
+ "skin/windows/editDialog.css",
+ ]
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/address/browser.ini",
+ "test/browser/browser.ini",
+ "test/browser/creditCard/browser.ini",
+ "test/browser/focus-leak/browser.ini",
+ "test/browser/heuristics/browser.ini",
+ "test/browser/heuristics/third_party/browser.ini",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "test/unit/xpcshell.ini",
+]
+
+MOCHITEST_MANIFESTS += [
+ "test/mochitest/creditCard/mochitest.ini",
+ "test/mochitest/mochitest.ini",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+SPHINX_TREES["docs"] = "docs"
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Form Autofill")
diff --git a/browser/extensions/formautofill/schema.json b/browser/extensions/formautofill/schema.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/browser/extensions/formautofill/schema.json
@@ -0,0 +1 @@
+[]
diff --git a/browser/extensions/formautofill/skin/linux/autocomplete-item.css b/browser/extensions/formautofill/skin/linux/autocomplete-item.css
new file mode 100644
index 0000000000..8f782aaa2a
--- /dev/null
+++ b/browser/extensions/formautofill/skin/linux/autocomplete-item.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+
+
+.autofill-item-box {
+ --default-font-size: 14.25;
+}
diff --git a/browser/extensions/formautofill/skin/linux/editDialog.css b/browser/extensions/formautofill/skin/linux/editDialog.css
new file mode 100644
index 0000000000..0f42d34b46
--- /dev/null
+++ b/browser/extensions/formautofill/skin/linux/editDialog.css
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Linux specific rules */
+dialog[subdialog] body {
+ font-size: 0.85rem;
+}
diff --git a/browser/extensions/formautofill/skin/osx/autocomplete-item.css b/browser/extensions/formautofill/skin/osx/autocomplete-item.css
new file mode 100644
index 0000000000..121c1139da
--- /dev/null
+++ b/browser/extensions/formautofill/skin/osx/autocomplete-item.css
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+
+/* On Mac, the autocomplete panel changes color in system dark mode. We need
+ to change the contrast on warning-background-color accordingly. */
+@media (prefers-color-scheme: dark) {
+ .autofill-item-box {
+ --warning-background-color: rgba(248,232,28,.6);
+ }
+ }
+
+
+.autofill-item-box {
+ --default-font-size: 11;
+}
diff --git a/browser/extensions/formautofill/skin/osx/editDialog.css b/browser/extensions/formautofill/skin/osx/editDialog.css
new file mode 100644
index 0000000000..e22c07ec95
--- /dev/null
+++ b/browser/extensions/formautofill/skin/osx/editDialog.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* OSX specific rules */
diff --git a/browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css b/browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css
new file mode 100644
index 0000000000..d9ec6bb665
--- /dev/null
+++ b/browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+
+xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box {
+ background-color: SelectedItem;
+ color: SelectedItemText;
+}
+
+xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-item-box > .autofill-button,
+xul|richlistitem[originaltype="autofill-clear-button"][selected="true"] > .autofill-item-box > .autofill-button {
+ background-color: ButtonHighlight;
+}
+
+xul|richlistitem[originaltype="autofill-insecureWarning"] {
+ border-bottom: 1px solid var(--panel-separator-color);
+ background-color: var(--arrowpanel-dimmed);
+}
+
+.autofill-item-box {
+ --item-padding-vertical: 7px;
+ --item-padding-horizontal: 10px;
+ --col-spacer: 7px;
+ --item-width: calc(50% - (var(--col-spacer) / 2));
+ --comment-text-color: GreyText;
+ --warning-text-color: GreyText;
+ --warning-background-color: rgba(248, 232, 28, .2);
+
+ --default-font-size: 12;
+ --label-affix-font-size: 10;
+ --label-font-size: 12;
+ --comment-font-size: 10;
+ --warning-font-size: 10;
+ --btn-font-size: 11;
+}
+
+.autofill-item-box[size="small"] {
+ --item-padding-vertical: 7px;
+ --col-spacer: 0px;
+ --row-spacer: 3px;
+ --item-width: 100%;
+}
+
+.autofill-item-box:not([ac-image=""]) {
+ --item-padding-vertical: 6.5px;
+ --comment-font-size: 11;
+}
+
+.autofill-footer,
+.autofill-footer[size="small"] {
+ --item-width: 100%;
+ --item-padding-vertical: 0;
+ --item-padding-horizontal: 0;
+}
+
+.autofill-item-box {
+ box-sizing: border-box;
+ margin: 0;
+ border-bottom: 1px solid rgba(38,38,38,.15);
+ padding: var(--item-padding-vertical) 0;
+ padding-inline: var(--item-padding-horizontal);
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ background-color: Field;
+ color: FieldText;
+}
+
+.autofill-item-box:last-child {
+ border-bottom: 0;
+}
+
+.autofill-item-box > .profile-item-col {
+ box-sizing: border-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: var(--item-width);
+}
+
+.autofill-item-box > .profile-label-col {
+ text-align: start;
+}
+
+.autofill-item-box:not([ac-image=""]) > .profile-label-col::before {
+ margin-inline-end: 5px;
+ float: inline-start;
+ content: "";
+ width: 16px;
+ height: 16px;
+ background-image: var(--primary-icon);
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ -moz-context-properties: fill;
+ fill: var(--comment-text-color)
+}
+
+.autofill-item-box > .profile-label-col > .profile-label {
+ font-size: calc(var(--label-font-size) / var(--default-font-size) * 1em);
+ unicode-bidi: plaintext;
+}
+
+.autofill-item-box > .profile-label-col > .profile-label-affix {
+ font-weight: lighter;
+ font-size: calc(var(--label-affix-font-size) / var(--default-font-size) * 1em);
+}
+
+.autofill-item-box > .profile-comment-col {
+ margin-inline-start: var(--col-spacer);
+ text-align: end;
+ color: var(--comment-text-color);
+}
+
+.autofill-item-box > .profile-comment-col > .profile-comment {
+ font-size: calc(var(--comment-font-size) / var(--default-font-size) * 1em);
+ unicode-bidi: plaintext;
+}
+
+.autofill-item-box[size="small"] {
+ flex-direction: column;
+}
+
+.autofill-item-box[size="small"] > .profile-comment-col {
+ margin-top: var(--row-spacer);
+ text-align: start;
+}
+
+.autofill-footer {
+ padding: 0;
+ flex-direction: column;
+}
+
+.autofill-footer > .autofill-footer-row {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: var(--item-width);
+}
+
+.autofill-footer > .autofill-warning {
+ padding: 2.5px 0;
+ color: var(--warning-text-color);
+ text-align: center;
+ background-color: var(--warning-background-color);
+ border-bottom: 1px solid rgba(38,38,38,.15);
+ font-size: calc(var(--warning-font-size) / var(--default-font-size) * 1em);
+}
+
+.autofill-footer > .autofill-button {
+ box-sizing: border-box;
+ padding: 0 10px;
+ min-height: 40px;
+ background-color: ButtonFace;
+ font-size: calc(var(--btn-font-size) / var(--default-font-size) * 1em);
+ color: ButtonText;
+ text-align: center;
+}
+
+.autofill-footer[no-warning="true"] > .autofill-warning {
+ display: none;
+}
+
+.autofill-insecure-item {
+ box-sizing: border-box;
+ padding: 4px 0;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ color: GrayText;
+}
+
+.autofill-insecure-item::before {
+ display: block;
+ margin-inline: 4px 8px;
+ content: "";
+ width: 16px;
+ height: 16px;
+ background-image: url(chrome://global/skin/icons/security-broken.svg);
+ -moz-context-properties: fill;
+ fill: GrayText;
+}
diff --git a/browser/extensions/formautofill/skin/shared/editAddress.css b/browser/extensions/formautofill/skin/shared/editAddress.css
new file mode 100644
index 0000000000..ea00f1a6a6
--- /dev/null
+++ b/browser/extensions/formautofill/skin/shared/editAddress.css
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.editAddressForm {
+ display: flex;
+ flex-wrap: wrap;
+ /* Use space-between so --grid-column-row-gap is in between the elements on a row */
+ justify-content: space-between;
+}
+
+dialog:not([subdialog]) .editAddressForm {
+ margin-inline: calc(var(--grid-column-row-gap) / -2);
+}
+
+.editAddressForm .container {
+ /* !important is needed to override preferences.css's generic label rule. */
+ margin-top: var(--grid-column-row-gap) !important;
+ margin-inline: calc(var(--grid-column-row-gap) / 2);
+ flex-grow: 1;
+}
+
+#country-container {
+ /* The country dropdown has a different intrinsic (content) width than the
+ other fields which are <input>. */
+ flex-basis: calc(50% - var(--grid-column-row-gap));
+ flex-grow: 0;
+ /* Country names can be longer than 50% which ruins the symmetry in the grid. */
+ max-width: calc(50% - var(--grid-column-row-gap));
+}
+
+
+/* Begin name field rules */
+
+#name-container input {
+ /* Override the default @size="20" on <input>, which acts like a min-width, not
+ * allowing the fields to shrink with flexbox as small as they need to to match
+ * the other rows. This is noticeable on narrow viewports e.g. in the
+ * PaymentRequest dialog on Linux due to the larger font-size. */
+ width: 0;
+}
+
+/* When there is focus within any of the name fields, the border of the inputs
+ * should be the focused color, except for inner ones which get overriden below. */
+#name-container:focus-within input {
+ border-color: var(--in-content-focus-outline-color);
+}
+
+/* Invalid name fields should show the error outline instead of the focus border */
+#name-container:focus-within input:-moz-ui-invalid {
+ border-color: transparent;
+}
+
+#given-name-container,
+#additional-name-container,
+#family-name-container {
+ display: flex;
+ /* The 3 pieces inside the name container don't have the .container class so
+ need to set flex-grow themselves. See `.editAddressForm .container` */
+ flex-grow: 1;
+ /* Remove the bottom margin from the name containers so that the outer
+ #name-container provides the margin on the outside */
+ margin-bottom: 0 !important;
+ margin-inline: 0;
+}
+
+#given-name-container:focus-within,
+#additional-name-container:focus-within,
+#family-name-container:focus-within {
+ z-index: 1;
+}
+
+/* The name fields are placed adjacent to each other.
+ Remove the border-radius on adjacent fields. */
+#given-name:dir(ltr),
+#family-name:dir(rtl) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right-width: 0;
+}
+
+#given-name:dir(rtl),
+#family-name:dir(ltr) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left-width: 0;
+}
+
+#additional-name {
+ border-radius: 0;
+ /* This provides the inner separators between the fields and should never
+ * change to the focused color. */
+ border-inline-color: var(--in-content-box-border-color) !important;
+}
+
+/* Since the name fields are adjacent, there isn't room for the -moz-ui-invalid
+ box-shadow so raise invalid name fields and their labels above the siblings
+ so the shadow is shown around all 4 sides. */
+#name-container input:-moz-ui-invalid,
+#name-container input:-moz-ui-invalid ~ .label-text {
+ z-index: 1;
+}
+
+/* End name field rules */
+
+#name-container,
+#street-address-container {
+ /* Name and street address are always full-width */
+ flex: 0 1 100%;
+}
+
+#street-address {
+ resize: vertical;
+}
+
+#country-warning-message {
+ box-sizing: border-box;
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ text-align: start;
+ opacity: .5;
+ padding-inline-start: 1em;
+ flex: 1;
+}
+
+dialog:not([subdialog]) #country-warning-message {
+ display: none;
+}
+
+moz-button-group{
+ margin-inline: 4px;
+ margin-block-end: 4px;
+}
diff --git a/browser/extensions/formautofill/skin/shared/editCreditCard.css b/browser/extensions/formautofill/skin/shared/editCreditCard.css
new file mode 100644
index 0000000000..00dde3b5b5
--- /dev/null
+++ b/browser/extensions/formautofill/skin/shared/editCreditCard.css
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.editCreditCardForm {
+ display: grid;
+ grid-template-areas:
+ "cc-number cc-exp-month cc-exp-year"
+ "cc-name cc-csc ."
+ "billingAddressGUID billingAddressGUID billingAddressGUID";
+ grid-template-columns: 4fr 2fr 2fr;
+ grid-row-gap: var(--grid-column-row-gap);
+ grid-column-gap: var(--grid-column-row-gap);
+}
+
+.editCreditCardForm label {
+ /* Remove the margin on these labels since they are styled on top of
+ the input/select element. */
+ margin-inline-start: 0;
+ margin-inline-end: 0;
+}
+
+.editCreditCardForm .container {
+ display: flex;
+}
+
+#cc-number-container {
+ grid-area: cc-number;
+}
+
+#cc-exp-month-container {
+ grid-area: cc-exp-month;
+}
+
+#cc-exp-year-container {
+ grid-area: cc-exp-year;
+}
+
+#cc-name-container {
+ grid-area: cc-name;
+}
+
+#cc-csc-container {
+ grid-area: cc-csc;
+}
+
+#billingAddressGUID-container {
+ grid-area: billingAddressGUID;
+}
+
+#billingAddressGUID {
+ grid-area: dropdown;
+}
diff --git a/browser/extensions/formautofill/skin/shared/editDialog-shared.css b/browser/extensions/formautofill/skin/shared/editDialog-shared.css
new file mode 100644
index 0000000000..c2ddf71a60
--- /dev/null
+++ b/browser/extensions/formautofill/skin/shared/editDialog-shared.css
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:root {
+ --in-field-label-size: .8em;
+ --grid-column-row-gap: 8px;
+ /* Use the animation-easing-function that is defined in xul.css. */
+ --animation-easing-function: cubic-bezier(.07,.95,0,1);
+}
+
+dialog[subdialog] form {
+ /* Add extra space to ensure invalid input box is displayed properly */
+ padding: 2px;
+}
+
+/* The overly specific input attributes are required to override
+ padding from common.css */
+form input[type="email"],
+form input[type="tel"],
+form input[type="text"],
+form textarea,
+form select {
+ flex-grow: 1;
+ padding-top: calc(var(--in-field-label-size) + .4em);
+}
+
+form input[type="tel"] {
+ text-align: match-parent;
+}
+
+select {
+ margin: 0;
+ padding-bottom: 5px;
+}
+
+form label[class="container"] select {
+ min-width: 0;
+}
+
+form label,
+form div {
+ /* Positioned so that the .label-text and .error-text children will be
+ positioned relative to this. */
+ position: relative;
+ display: block;
+ line-height: 1em;
+}
+
+/* Reset margins for inputs and textareas, overriding in-content styles */
+#form textarea,
+#form input {
+ margin: 0;
+}
+
+form :is(label, div) .label-text {
+ position: absolute;
+ opacity: .5;
+ pointer-events: none;
+ inset-inline-start: 10px;
+ top: .2em;
+ transition: top .2s var(--animation-easing-function),
+ font-size .2s var(--animation-easing-function);
+}
+
+form :is(label, div):focus-within .label-text,
+form :is(label, div) .label-text[field-populated] {
+ top: 0;
+ font-size: var(--in-field-label-size);
+}
+
+form :is(input, select, textarea):focus ~ .label-text {
+ color: var(--in-content-item-selected);
+ opacity: 1;
+}
+
+/* Focused error fields should get a darker text but not the blue one since it
+ * doesn't look good with the red error outline. */
+form :is(input, select, textarea):focus:-moz-ui-invalid ~ .label-text {
+ color: var(--in-content-text-color);
+}
+
+form div[required] > label .label-text::after,
+form :is(label, div)[required] .label-text::after {
+ content: attr(fieldRequiredSymbol);
+}
+
+.persist-checkbox label {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-block: var(--grid-column-row-gap);
+}
+
+dialog[subdialog] form {
+ /* Match the margin-inline-start of the #controls-container buttons
+ and provide enough padding at the top of the form so button outlines
+ don't get clipped. */
+ padding: 4px 4px 0;
+}
+
+#controls-container {
+ display: flex;
+ justify-content: end;
+ margin: 1em 0 0;
+}
+
+#billingAddressGUID-container {
+ display: none;
+}
diff --git a/browser/extensions/formautofill/skin/windows/autocomplete-item.css b/browser/extensions/formautofill/skin/windows/autocomplete-item.css
new file mode 100644
index 0000000000..ec5e87d925
--- /dev/null
+++ b/browser/extensions/formautofill/skin/windows/autocomplete-item.css
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+.autofill-item-box {
+ --default-font-size: 12;
+}
+
+xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-item-box > .autofill-button,
+xul|richlistitem[originaltype="autofill-clear-button"][selected="true"] > .autofill-item-box > .autofill-button {
+ background-color: color-mix(in srgb, Field 90%, FieldText);
+}
+
+@media (-moz-windows-default-theme: 0) {
+ xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box {
+ background-color: SelectedItem;
+ }
+
+ .autofill-item-box {
+ --comment-text-color: GrayText;
+ }
+}
diff --git a/browser/extensions/formautofill/skin/windows/editDialog.css b/browser/extensions/formautofill/skin/windows/editDialog.css
new file mode 100644
index 0000000000..334e67cfc7
--- /dev/null
+++ b/browser/extensions/formautofill/skin/windows/editDialog.css
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* The save button should be on the left and cancel on the right for Windows */
+#controlsContainer > #save {
+ order: -1;
+}
+
+#controlsContainer > #cancel {
+ order: 1;
+}
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 <browser>");
+ 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 = `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="organization" autocomplete="organization">
+ <select id="country""" autocomplete="country">
+ <option value="Germany">Germany</option>
+ <option value="Canada">Canada</option>
+ <option value="United States">United States</option>
+ <option value="France">France</option>
+ </select>
+ </body></html>
+`;
+
+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: `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ 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>"
+ );
+ // 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 <select>'s incremental search is cleared.
+ EventUtils.synthesizeKey("VK_ACCEPT", {}, win);
+ }
+
+ doc.querySelector("#cancel").click();
+ });
+});
+
+add_task(async function test_combined_name_fields() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ let givenNameField = doc.querySelector("#given-name");
+ let addtlNameField = doc.querySelector("#additional-name");
+ let familyNameField = doc.querySelector("#family-name");
+
+ function getComputedPropertyValue(field, property) {
+ return win.getComputedStyle(field).getPropertyValue(property);
+ }
+ function checkNameComputedPropertiesMatch(
+ field,
+ property,
+ value,
+ checkFn = is
+ ) {
+ checkFn(
+ getComputedPropertyValue(field, property),
+ value,
+ `Check ${field.id}'s ${property} is ${value}`
+ );
+ }
+ function checkNameFieldBorders(borderColorUnfocused, borderColorFocused) {
+ info("checking the perimeter colors");
+ checkNameComputedPropertiesMatch(
+ givenNameField,
+ "border-top-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ addtlNameField,
+ "border-top-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ familyNameField,
+ "border-top-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ familyNameField,
+ "border-right-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ givenNameField,
+ "border-bottom-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ addtlNameField,
+ "border-bottom-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ familyNameField,
+ "border-bottom-color",
+ borderColorFocused
+ );
+ checkNameComputedPropertiesMatch(
+ givenNameField,
+ "border-left-color",
+ borderColorFocused
+ );
+
+ info("checking the internal borders");
+ checkNameComputedPropertiesMatch(
+ givenNameField,
+ "border-right-width",
+ "0px"
+ );
+ checkNameComputedPropertiesMatch(
+ addtlNameField,
+ "border-left-width",
+ "2px"
+ );
+ checkNameComputedPropertiesMatch(
+ addtlNameField,
+ "border-left-color",
+ borderColorFocused,
+ isnot
+ );
+ checkNameComputedPropertiesMatch(
+ addtlNameField,
+ "border-right-width",
+ "2px"
+ );
+ checkNameComputedPropertiesMatch(
+ addtlNameField,
+ "border-right-color",
+ borderColorFocused,
+ isnot
+ );
+ checkNameComputedPropertiesMatch(
+ familyNameField,
+ "border-left-width",
+ "0px"
+ );
+ }
+
+ // Set these variables since the test doesn't run from a subdialog and
+ // therefore doesn't get the additional common CSS files injected.
+ let borderColor = "rgb(0, 255, 0)";
+ let borderColorFocused = "rgb(0, 0, 255)";
+ doc.body.style.setProperty("--in-content-box-border-color", borderColor);
+ doc.body.style.setProperty(
+ "--in-content-focus-outline-color",
+ borderColorFocused
+ );
+
+ givenNameField.focus();
+ checkNameFieldBorders(borderColor, borderColorFocused);
+
+ addtlNameField.focus();
+ checkNameFieldBorders(borderColor, borderColorFocused);
+
+ familyNameField.focus();
+ checkNameFieldBorders(borderColor, borderColorFocused);
+
+ info("unfocusing the name fields");
+ let cancelButton = doc.querySelector("#cancel");
+ cancelButton.focus();
+ borderColor = getComputedPropertyValue(givenNameField, "border-top-color");
+ isnot(
+ borderColor,
+ borderColorFocused,
+ "Check that the border color is different"
+ );
+ checkNameFieldBorders(borderColor, borderColor);
+
+ cancelButton.click();
+ });
+});
+
+add_task(async function test_combined_name_fields_error() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ let givenNameField = doc.querySelector("#given-name");
+ info("mark the given name field as invalid");
+ givenNameField.value = "";
+ givenNameField.focus();
+ ok(
+ givenNameField.matches(":-moz-ui-invalid"),
+ "Check field is visually invalid"
+ );
+
+ let givenNameLabel = doc.querySelector("#given-name-container .label-text");
+ // Override pointer-events so that we can use elementFromPoint to know if
+ // the label text is visible.
+ givenNameLabel.style.pointerEvents = "auto";
+ let givenNameLabelRect = givenNameLabel.getBoundingClientRect();
+ // Get the center of the label
+ let el = doc.elementFromPoint(
+ givenNameLabelRect.left + givenNameLabelRect.width / 2,
+ givenNameLabelRect.top + givenNameLabelRect.height / 2
+ );
+
+ is(
+ el,
+ givenNameLabel,
+ "Check that the label text is visible in the error state"
+ );
+ is(
+ win.getComputedStyle(givenNameField).getPropertyValue("border-top-color"),
+ "rgba(0, 0, 0, 0)",
+ "Border should be transparent so that only the error outline shows"
+ );
+ doc.querySelector("#cancel").click();
+ });
+});
+
+add_task(async function test_hiddenFieldNotSaved() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ doc.querySelector("#address-level2").focus();
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
+ doc.querySelector("#address-level1").focus();
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"], {}, win);
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Germany", {}, win);
+ doc.querySelector("#save").focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let addresses = await getAddresses();
+ is(addresses[0].country, "DE", "check country");
+ is(
+ addresses[0]["address-level2"],
+ TEST_ADDRESS_1["address-level2"],
+ "check address-level2"
+ );
+ is(
+ addresses[0]["address-level1"],
+ undefined,
+ "address-level1 should not be saved"
+ );
+
+ await removeAllRecords();
+});
+
+add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
+ let addresses = await getAddresses();
+ ok(!addresses.length, "no addresses at start of test");
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ doc.querySelector("#address-level2").focus();
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
+ doc.querySelector("#address-level1").focus();
+ while (
+ doc.querySelector("#address-level1").value !=
+ TEST_ADDRESS_1["address-level1"]
+ ) {
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"][0], {}, win);
+ }
+ doc.querySelector("#save").focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ addresses = await getAddresses();
+ is(addresses[0].country, "US", "check country");
+ is(
+ addresses[0]["address-level2"],
+ TEST_ADDRESS_1["address-level2"],
+ "check address-level2"
+ );
+ is(
+ addresses[0]["address-level1"],
+ TEST_ADDRESS_1["address-level1"],
+ "check address-level1"
+ );
+
+ await testDialog(
+ EDIT_ADDRESS_DIALOG_URL,
+ win => {
+ let doc = win.document;
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Germany", {}, win);
+ 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-level2"],
+ TEST_ADDRESS_1["address-level2"],
+ "check address-level2"
+ );
+ is(
+ addresses[0]["address-level1"],
+ undefined,
+ "address-level1 should be removed"
+ );
+ is(addresses[0].country, "DE", "country changed");
+ await removeAllRecords();
+});
+
+add_task(async function test_countrySpecificFieldsGetRequiredness() {
+ Region._setHomeRegion("RO", false);
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ is(
+ doc.querySelector("#country").value,
+ "RO",
+ "Default country set to Romania"
+ );
+ let provinceField = doc.getElementById("address-level1");
+ ok(
+ !provinceField.required,
+ "address-level1 should not be marked as required"
+ );
+ ok(provinceField.disabled, "address-level1 should be marked as disabled");
+ is(
+ provinceField.parentNode.style.display,
+ "none",
+ "address-level1 is hidden for Romania"
+ );
+
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("United States", {}, win);
+
+ await TestUtils.waitForCondition(
+ () => {
+ provinceField = doc.getElementById("address-level1");
+ return provinceField.parentNode.style.display != "none";
+ },
+ "Wait for address-level1 to become visible",
+ 10
+ );
+
+ ok(provinceField.required, "address-level1 should be marked as required");
+ ok(
+ !provinceField.disabled,
+ "address-level1 should not be marked as disabled"
+ );
+
+ // Dispatch a dummy key event so that <select>'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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ idsToShowPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description: "without @autocomplete - all fields in the same form",
+ document: `<form>
+ <input id="cc-number" placeholder="credit card number">
+ <input id="cc-name" placeholder="credit card holder name">
+ <input id="cc-exp" placeholder="expiration date">
+ </form>`,
+ idsToShowPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description: "@autocomplete - each field in its own form",
+ document: `<form><input id="cc-number" autocomplete="cc-number"></form>
+ <form><input id="cc-name" autocomplete="cc-name"></form>
+ <form><input id="cc-exp" autocomplete="cc-exp"></form>`,
+ idsToShowPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description:
+ "without @autocomplete - each field in its own form (high-confidence cc-number & cc-name)",
+ document: `<form><input id="cc-number" placeholder="credit card number"></form>
+ <form><input id="cc-name" placeholder="credit card holder name"></form>
+ <form><input id="cc-exp" placeholder="expiration date"></form>`,
+ prefs: [
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ "0.9",
+ ],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "0.95",
+ ],
+ ],
+ idsToShowPopup: ["cc-number", "cc-name"],
+ idsWithNoPopup: ["cc-exp"],
+ },
+ {
+ description:
+ "without @autocomplete - each field in its own form (normal-confidence cc-number & cc-name)",
+ document: `<form><input id="cc-number" placeholder="credit card number"></form>
+ <form><input id="cc-name" placeholder="credit card holder name"></form>
+ <form><input id="cc-exp" placeholder="expiration date"></form>`,
+ prefs: [
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ "0.9",
+ ],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "0.8",
+ ],
+ ],
+ idsWithNoPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description:
+ "with @autocomplete - cc-number/cc-name and another <input> in a form",
+ document: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="password" type="password">
+ </form>
+ <form>
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="password" type="password">
+ </form>`,
+ idsToShowPopup: ["cc-number", "cc-name"],
+ },
+ {
+ description:
+ "without @autocomplete - high-confidence cc-number/cc-name and another <input> in a form",
+ document: `<form>
+ <input id="cc-number" placeholder="credit card number">
+ <input id="password" type="password">
+ </form>
+ <form>
+ <input id="cc-name" placeholder="credit card holder name">
+ <input id="password" type="password">
+ </form>`,
+ idsWithNoPopup: ["cc-number", "cc-name"],
+ },
+ {
+ description:
+ "without @autocomplete - high-confidence cc-number/cc-name and another hidden <input> in a form",
+ document: `<form>
+ <input id="cc-number" placeholder="credit card number">
+ <input id="token" type="hidden">
+ </form>
+ <form>
+ <input id="cc-name" placeholder="credit card holder name">
+ <input id="token" type="hidden">
+ </form>`,
+ prefs: [
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ "0.9",
+ ],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "0.95",
+ ],
+ ],
+ idsToShowPopup: ["cc-number", "cc-name"],
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+});
+
+add_task(async function test_heuristics() {
+ for (const TEST of TESTCASES) {
+ info(`Test ${TEST.description}`);
+ if (TEST.prefs) {
+ await SpecialPowers.pushPrefEnv({ set: TEST.prefs });
+ }
+
+ await BrowserTestUtils.withNewTab(EMPTY_URL, async function (browser) {
+ await SpecialPowers.spawn(browser, [TEST.document], doc => {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.body.innerHTML = doc;
+ });
+
+ await SimpleTest.promiseFocus(browser);
+
+ let ids = TEST.idsToShowPopup ?? [];
+ for (const id of ids) {
+ await runAndWaitForAutocompletePopupOpen(browser, async () => {
+ await focusAndWaitForFieldsIdentified(browser, `#${id}`);
+ });
+ ok(true, `popup is opened on <input id=${id}>`);
+ }
+
+ ids = TEST.idsWithNoPopup ?? [];
+ for (const id of ids) {
+ await focusAndWaitForFieldsIdentified(browser, `#${id}`);
+ await ensureNoAutocompletePopup(browser);
+ }
+ });
+
+ if (TEST.prefs) {
+ await SpecialPowers.popPrefEnv();
+ }
+ }
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js
new file mode 100644
index 0000000000..e3f12096c2
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PROFILE = {
+ "cc-name": "John Doe",
+ "cc-number": "4111111111111111",
+ // "cc-type" should be remove from proile after fixing Bug 1834768.
+ "cc-type": "visa",
+ "cc-exp-month": 4,
+ "cc-exp-year": new Date().getFullYear(),
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ],
+ });
+});
+
+add_autofill_heuristic_tests([
+ {
+ description:
+ "cc-type select does not have any information in labels or attributes",
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <select id="test">
+ <option value="" selected="">0</option>
+ <option value="VISA">1</option>
+ <option value="MasterCard">2</option>
+ <option value="DINERS">3</option>
+ <option value="Discover">4</option>
+ </select>
+ </form>
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <select id="test">
+ <option value="0" selected="">Card Type</option>
+ <option value="1">Visa</option>
+ <option value="2">MasterCard</option>
+ <option value="3">Diners Club International</option>
+ <option value="4">Discover</option>
+ </select>
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ description: "cc-type option.value has the hint",
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
+ { fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
+ { fieldName: "cc-type", reason: "regex-heuristic", autofill: "VISA" },
+ ],
+ },
+ {
+ description: "cc-type option.text has the hint",
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
+ { fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
+ { fieldName: "cc-type", reason: "regex-heuristic", autofill: "1" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js
new file mode 100644
index 0000000000..825a2978de
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_autodetect_credit_not_set() {
+ const testCard = {
+ "cc-name": "John Doe",
+ "cc-number": "4012888888881881",
+ "cc-exp-month": "06",
+ "cc-exp-year": "2044",
+ };
+ const expectedData = {
+ ...testCard,
+ ...{ "cc-type": "visa" },
+ };
+ let onChanged = waitForStorageChangedEvents("add");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": testCard["cc-exp-month"],
+ "#cc-exp-year": testCard["cc-exp-year"],
+ "#cc-type": testCard["cc-type"],
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ let decryptedNumber = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+ savedCreditCard["cc-number"] = decryptedNumber;
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_autodetect_credit_overwrite() {
+ const testCard = {
+ "cc-name": "John Doe",
+ "cc-number": "4012888888881881",
+ "cc-exp-month": "06",
+ "cc-exp-year": "2044",
+ "cc-type": "master", // Wrong credit card type
+ };
+ const expectedData = {
+ ...testCard,
+ ...{ "cc-type": "visa" },
+ };
+ let onChanged = waitForStorageChangedEvents("add");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": testCard["cc-exp-month"],
+ "#cc-exp-year": testCard["cc-exp-year"],
+ "#cc-type": testCard["cc-type"],
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ let decryptedNumber = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+ savedCreditCard["cc-number"] = decryptedNumber;
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js
new file mode 100644
index 0000000000..76e125a196
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We want to ensure that non-normalized credit card data is normalized
+// correctly as part of the save credit card flow
+add_task(async function test_new_submitted_card_is_normalized() {
+ const testCard = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "4",
+ "cc-exp-year": "25",
+ };
+ const expectedData = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "4",
+ // cc-exp-year should be normalized to 2025
+ "cc-exp-year": "2025",
+ };
+ let onChanged = waitForStorageChangedEvents("add");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": testCard["cc-exp-month"],
+ "#cc-exp-year": testCard["cc-exp-year"],
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ let decryptedNumber = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+ savedCreditCard["cc-number"] = decryptedNumber;
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_updated_card_is_normalized() {
+ const testCard = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "11",
+ "cc-exp-year": "20",
+ };
+ await saveCreditCard(testCard);
+ const expectedData = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "10",
+ // cc-exp-year should be normalized to 2027
+ "cc-exp-year": "2027",
+ };
+ let onChanged = waitForStorageChangedEvents("update");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": "10",
+ "#cc-exp-year": "27",
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ savedCreditCard["cc-number"] = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
new file mode 100644
index 0000000000..e8790243ac
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
@@ -0,0 +1,872 @@
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const CC_NUM_USES_HISTOGRAM = "CREDITCARD_NUM_USES";
+
+function ccFormArgsv1(method, extra) {
+ return ["creditcard", method, "cc_form", undefined, extra];
+}
+
+function ccFormArgsv2(method, extra) {
+ return ["creditcard", method, "cc_form_v2", undefined, extra];
+}
+
+function buildccFormv2Extra(extra, defaultValue) {
+ let defaults = {};
+ for (const field of [
+ "cc_name",
+ "cc_number",
+ "cc_type",
+ "cc_exp",
+ "cc_exp_month",
+ "cc_exp_year",
+ ]) {
+ defaults[field] = defaultValue;
+ }
+
+ return { ...defaults, ...extra };
+}
+
+async function assertTelemetry(expected_content, expected_parent) {
+ let snapshots;
+
+ info(
+ `Waiting for ${expected_content?.length ?? 0} content events and ` +
+ `${expected_parent?.length ?? 0} parent events`
+ );
+
+ await TestUtils.waitForCondition(
+ () => {
+ snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ return (
+ (snapshots.parent?.length ?? 0) >= (expected_parent?.length ?? 0) &&
+ (snapshots.content?.length ?? 0) >= (expected_content?.length ?? 0)
+ );
+ },
+ "Wait for telemetry to be collected",
+ 100,
+ 100
+ );
+
+ info(JSON.stringify(snapshots, null, 2));
+
+ if (expected_content !== undefined) {
+ expected_content = expected_content.map(
+ ([category, method, object, value, extra]) => {
+ return { category, method, object, value, extra };
+ }
+ );
+
+ let clear = expected_parent === undefined;
+
+ TelemetryTestUtils.assertEvents(
+ expected_content,
+ {
+ category: "creditcard",
+ },
+ { clear, process: "content" }
+ );
+ }
+
+ if (expected_parent !== undefined) {
+ expected_parent = expected_parent.map(
+ ([category, method, object, value, extra]) => {
+ return { category, method, object, value, extra };
+ }
+ );
+ TelemetryTestUtils.assertEvents(
+ expected_parent,
+ {
+ category: "creditcard",
+ },
+ { process: "parent" }
+ );
+ }
+}
+
+async function assertHistogram(histogramId, expectedNonZeroRanges) {
+ let actualNonZeroRanges = {};
+ await TestUtils.waitForCondition(
+ () => {
+ const snapshot = Services.telemetry
+ .getHistogramById(histogramId)
+ .snapshot();
+ // Compute the actual ranges in the format { range1: value1, range2: value2 }.
+ for (let [range, value] of Object.entries(snapshot.values)) {
+ if (value > 0) {
+ actualNonZeroRanges[range] = value;
+ }
+ }
+
+ return (
+ JSON.stringify(actualNonZeroRanges) ==
+ JSON.stringify(expectedNonZeroRanges)
+ );
+ },
+ "Wait for telemetry to be collected",
+ 100,
+ 100
+ );
+
+ Assert.equal(
+ JSON.stringify(actualNonZeroRanges),
+ JSON.stringify(expectedNonZeroRanges)
+ );
+}
+
+async function openTabAndUseCreditCard(
+ idx,
+ creditCard,
+ { closeTab = true, submitForm = true } = {}
+) {
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ CREDITCARD_FORM_URL
+ );
+ let browser = tab.linkedBrowser;
+
+ await openPopupOn(browser, "form #cc-name");
+ for (let i = 0; i <= idx; i++) {
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ }
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+ await waitForAutofill(browser, "#cc-number", creditCard["cc-number"]);
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#cc-number",
+ newValues: {},
+ },
+ submitForm
+ );
+
+ if (!closeTab) {
+ return tab;
+ }
+
+ await BrowserTestUtils.removeTab(tab);
+ return null;
+}
+
+add_setup(async function () {
+ Services.telemetry.setEventRecordingEnabled("creditcard", true);
+ registerCleanupFunction(async function () {
+ Services.telemetry.setEventRecordingEnabled("creditcard", false);
+ });
+});
+
+add_task(async function test_popup_opened() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"],
+ ],
+ });
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ const focusInput = "#cc-number";
+
+ await openPopupOn(browser, focusInput);
+
+ // Clean up
+ await closePopup(browser);
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 1,
+ "There should be 1 section detected."
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count"
+ );
+
+ await removeAllRecords();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_popup_opened_form_without_autocomplete() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "1",
+ ],
+ ],
+ });
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL },
+ async function (browser) {
+ const focusInput = "#cc-number";
+
+ await openPopupOn(browser, focusInput);
+
+ // Clean up
+ await closePopup(browser);
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2(
+ "detected",
+ buildccFormv2Extra({ cc_number: "1", cc_name: "1", cc_exp: "false" }, "0")
+ ),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 1,
+ "There should be 1 section detected."
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count"
+ );
+
+ await removeAllRecords();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(
+ async function test_popup_opened_form_without_autocomplete_separate_cc_number() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "1",
+ ],
+ ],
+ });
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ // Click on the cc-number field of the form that only contains a cc-number field
+ // (detected by Fathom)
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#form2-cc-number #cc-number");
+ await closePopup(browser);
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_number: "1" }, "false")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ // Then click on the cc-name field of the form that doesn't have a cc-number field
+ // (detected by regexp-based heuristic)
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#form2-cc-other #cc-name");
+ await closePopup(browser);
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2(
+ "detected",
+ buildccFormv2Extra(
+ { cc_name: "1", cc_type: "0", cc_exp_month: "0", cc_exp_year: "0" },
+ "false"
+ )
+ ),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 2,
+ "There should be 1 section detected."
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count"
+ );
+
+ await removeAllRecords();
+ await SpecialPowers.popPrefEnv();
+ }
+);
+
+add_task(async function test_submit_creditCard_new() {
+ async function test_per_command(
+ command,
+ idx,
+ useCount = {},
+ expectChanged = undefined
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ let onChanged;
+ if (expectChanged !== undefined) {
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed");
+ }
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "5038146897157463",
+ "#cc-exp-month": "12",
+ "#cc-exp-year": "2017",
+ "#cc-type": "mastercard",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(command, idx);
+ if (expectChanged !== undefined) {
+ await onChanged;
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "formautofill.creditCards.autofill_profiles_count",
+ expectChanged,
+ "There should be ${expectChanged} profile(s) stored."
+ );
+ }
+ }
+ );
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, useCount);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+
+ let expected_content = [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2(
+ "submitted",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "user_filled")
+ ),
+ ccFormArgsv1("submitted", {
+ // 5 fields plus submit button
+ fields_not_auto: "6",
+ fields_auto: "0",
+ fields_modified: "0",
+ }),
+ ];
+ await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1);
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "capture_doorhanger"],
+ ["creditcard", "save", "capture_doorhanger"],
+ ]);
+
+ await test_per_command(SECONDARY_BUTTON);
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "capture_doorhanger"],
+ ["creditcard", "cancel", "capture_doorhanger"],
+ ]);
+
+ await test_per_command(MENU_BUTTON, 0);
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "capture_doorhanger"],
+ ["creditcard", "disable", "capture_doorhanger"],
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 3,
+ "There should be 3 sections detected."
+ );
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count",
+ 3,
+ "There should be 1 section submitted."
+ );
+});
+
+add_task(async function test_submit_creditCard_autofill() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "1 credit card in storage");
+
+ await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1);
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 1: 1,
+ });
+
+ SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+
+ await assertTelemetry(
+ [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ccFormArgsv2(
+ "filled",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "filled")
+ ),
+ ccFormArgsv1("filled"),
+ ccFormArgsv2(
+ "submitted",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "autofilled")
+ ),
+ ccFormArgsv1("submitted", {
+ fields_not_auto: "3",
+ fields_auto: "5",
+ fields_modified: "0",
+ }),
+ ],
+ []
+ );
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_submit_creditCard_update() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ async function test_per_command(
+ command,
+ idx,
+ useCount = {},
+ expectChanged = undefined
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "1 credit card in storage");
+
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ let onChanged;
+ if (expectChanged !== undefined) {
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed");
+ }
+
+ await openPopupOn(browser, "form #cc-name");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+
+ await waitForAutofill(browser, "#cc-name", "John Doe");
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-exp-year": "2019",
+ },
+ });
+ await onPopupShown;
+ await clickDoorhangerButton(command, idx);
+ if (expectChanged !== undefined) {
+ await onChanged;
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "formautofill.creditCards.autofill_profiles_count",
+ expectChanged,
+ "There should be ${expectChanged} profile(s) stored."
+ );
+ }
+ }
+ );
+
+ await assertHistogram("CREDITCARD_NUM_USES", useCount);
+
+ SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+
+ await removeAllRecords();
+ }
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+
+ let expected_content = [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ccFormArgsv2(
+ "filled",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "filled")
+ ),
+ ccFormArgsv1("filled"),
+ ccFormArgsv2("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv2(
+ "submitted",
+ buildccFormv2Extra(
+ { cc_exp: "unavailable", cc_exp_year: "user_filled" },
+ "autofilled"
+ )
+ ),
+ ccFormArgsv1("submitted", {
+ fields_not_auto: "3",
+ fields_auto: "5",
+ fields_modified: "1",
+ }),
+ ];
+
+ await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1);
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "update_doorhanger"],
+ ["creditcard", "update", "update_doorhanger"],
+ ]);
+
+ await test_per_command(SECONDARY_BUTTON, undefined, { 0: 1, 1: 1 }, 2);
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "update_doorhanger"],
+ ["creditcard", "save", "update_doorhanger"],
+ ]);
+});
+
+const TEST_SELECTORS = {
+ selRecords: "#credit-cards",
+ btnRemove: "#remove",
+ btnAdd: "#add",
+ btnEdit: "#edit",
+};
+
+const DIALOG_SIZE = "width=600,height=400";
+
+add_task(async function test_removingCreditCardsViaKeyboardDelete() {
+ Services.telemetry.clearEvents();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ Assert.equal(selRecords.length, 1, "One credit card");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ Assert.equal(selRecords.length, 0, "No credit cards left");
+
+ win.close();
+
+ await assertTelemetry(undefined, [
+ ["creditcard", "show", "manage"],
+ ["creditcard", "delete", "manage"],
+ ]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_saveCreditCard() {
+ Services.telemetry.clearEvents();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ "0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_1["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+
+ await assertTelemetry(undefined, [["creditcard", "add", "manage"]]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_editCreditCard() {
+ Services.telemetry.clearEvents();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "only one credit card is in storage");
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+
+ await assertTelemetry(undefined, [
+ ["creditcard", "show_entry", "manage"],
+ ["creditcard", "edit", "manage"],
+ ]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_histogram() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+
+ await setStorage(
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3,
+ TEST_CREDIT_CARD_5
+ );
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 4, "3 credit cards in storage");
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 4,
+ });
+
+ await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 3,
+ 1: 1,
+ });
+
+ await openTabAndUseCreditCard(1, TEST_CREDIT_CARD_2);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 2,
+ 1: 2,
+ });
+
+ await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_2);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 2,
+ 1: 1,
+ 2: 1,
+ });
+
+ await openTabAndUseCreditCard(1, TEST_CREDIT_CARD_1);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 2,
+ 2: 2,
+ });
+
+ await openTabAndUseCreditCard(2, TEST_CREDIT_CARD_5);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 1,
+ 1: 1,
+ 2: 2,
+ });
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {});
+});
+
+add_task(async function test_clear_creditCard_autofill() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "1 credit card in storage");
+
+ let tab = await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1, {
+ closeTab: false,
+ submitForm: false,
+ });
+
+ let expected_content = [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ccFormArgsv2(
+ "filled",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "filled")
+ ),
+ ccFormArgsv1("filled"),
+ ];
+ await assertTelemetry(expected_content, []);
+ Services.telemetry.clearEvents();
+
+ let browser = tab.linkedBrowser;
+
+ let popupshown = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "shown"
+ );
+ // Already focus in "cc-number" field, press 'down' to bring to popup.
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser);
+ await popupshown;
+
+ expected_content = [
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ];
+ await assertTelemetry(expected_content, []);
+ Services.telemetry.clearEvents();
+
+ // kPress Clear Form.
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser);
+ await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser);
+
+ expected_content = [
+ ccFormArgsv2("filled_modified", { field_name: "cc-name" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-name" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-number" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-number" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-exp-month" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-exp-month" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-type" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-type" }),
+ ccFormArgsv2("cleared", { field_name: "cc-number" }),
+ // popup is shown again because when the field is cleared and is focused,
+ // we automatically triggers the popup.
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ];
+
+ await assertTelemetry(expected_content, []);
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.removeTab(tab);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js b/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js
new file mode 100644
index 0000000000..f532ea5da9
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js
@@ -0,0 +1,422 @@
+"use strict";
+
+add_setup(async function () {
+ let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+ await formAutofillStorage.initialize();
+});
+
+add_task(async function test_cancelEditCreditCardDialog() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ win.document.querySelector("#cancel").click();
+ });
+});
+
+add_task(async function test_cancelEditCreditCardDialogWithESC() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ });
+});
+
+add_task(async function test_saveCreditCard() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ ok(
+ win.document.documentElement
+ .querySelector("title")
+ .textContent.includes("Add"),
+ "Add card dialog title is correct"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ "0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ is(
+ win.document.activeElement.selectedOptions[0].text,
+ "04 - April",
+ "Displayed month should match number and name"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_1["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_1)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[0][fieldName], fieldValue, "check " + fieldName);
+ }
+ is(creditCards[0].billingAddressGUID, undefined, "check billingAddressGUID");
+ ok(creditCards[0]["cc-number-encrypted"], "cc-number-encrypted exists");
+});
+
+add_task(async function test_saveCreditCardWithMaxYear() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_2["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_2["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 2, "Two credit cards are in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_2)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[1][fieldName], fieldValue, "check " + fieldName);
+ }
+ ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists");
+ await removeCreditCards([creditCards[1].guid]);
+});
+
+add_task(async function test_saveCreditCardWithBillingAddress() {
+ await setStorage(TEST_ADDRESS_4, TEST_ADDRESS_1);
+ let addresses = await getAddresses();
+ let billingAddress = addresses[0];
+
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: undefined,
+ });
+
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(billingAddress["given-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 2, "Two credit cards are in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[1][fieldName], fieldValue, "check " + fieldName);
+ }
+ ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists");
+ await removeCreditCards([creditCards[1].guid]);
+ await removeAddresses([addresses[0].guid, addresses[1].guid]);
+});
+
+add_task(async function test_editCreditCard() {
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "only one credit card is in storage");
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ ok(
+ win.document.documentElement
+ .querySelector("title")
+ .textContent.includes("Edit"),
+ "Edit card dialog title is correct"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-name"],
+ TEST_CREDIT_CARD_1["cc-name"] + "test",
+ "cc name changed"
+ );
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_editCreditCardWithMissingBillingAddress() {
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: "unknown-guid",
+ });
+ await setStorage(TEST_CREDIT_CARD);
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "one credit card in storage");
+ is(
+ creditCards[0].billingAddressGUID,
+ TEST_CREDIT_CARD.billingAddressGUID,
+ "Check saved billingAddressGUID"
+ );
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-name"],
+ TEST_CREDIT_CARD["cc-name"] + "test",
+ "cc name changed"
+ );
+ is(
+ creditCards[0].billingAddressGUID,
+ undefined,
+ "unknown GUID removed upon manual save"
+ );
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_addInvalidCreditCard() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, async win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("test name", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.querySelector("#save"),
+ {},
+ win
+ );
+
+ is(
+ win.document.querySelector("form").checkValidity(),
+ false,
+ "cc-number is invalid"
+ );
+ await ensureCreditCardDialogNotClosed(win);
+ info("closing");
+ win.close();
+ });
+ info("closed");
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_editInvalidCreditCardNumber() {
+ await setStorage(TEST_ADDRESS_4);
+ let addresses = await getAddresses();
+ let billingAddress = addresses[0];
+
+ const INVALID_CREDIT_CARD_NUMBER = "123456789";
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: billingAddress.guid,
+ guid: "invalid-number",
+ version: 2,
+ "cc-number": INVALID_CREDIT_CARD_NUMBER,
+ });
+
+ // Directly use FormAutofillStorage so we can set
+ // sourceSync: true, since saveCreditCard uses FormAutofillParent
+ // which doesn't expose this option.
+ let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+ await formAutofillStorage.initialize();
+ // Use `sourceSync: true` to bypass field normalization which will
+ // fail due to the invalid credit card number.
+ await formAutofillStorage.creditCards.add(TEST_CREDIT_CARD, {
+ sourceSync: true,
+ });
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-number"],
+ "*********",
+ "invalid credit card number stored"
+ );
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ is(
+ win.document.querySelector("#cc-number").value,
+ INVALID_CREDIT_CARD_NUMBER,
+ "cc-number field should be showing invalid credit card number"
+ );
+ is(
+ win.document.querySelector("#cc-number").checkValidity(),
+ false,
+ "cc-number is invalid"
+ );
+ win.document.querySelector("#cancel").click();
+ },
+ {
+ record: creditCards[0],
+ skipDecryption: true,
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-number"],
+ "*********",
+ "invalid cc number still in record"
+ );
+ await removeCreditCards([creditCards[0].guid]);
+ await removeAddresses([addresses[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+ addresses = await getAddresses();
+ is(addresses.length, 0, "Address storage is empty");
+});
+
+add_task(async function test_editCreditCardWithInvalidNumber() {
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_1);
+ await setStorage(TEST_CREDIT_CARD);
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "only one credit card is in storage");
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ ok(
+ win.document.documentElement
+ .querySelector("title")
+ .textContent.includes("Edit"),
+ "Edit card dialog title is correct"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ is(
+ win.document.querySelector("#cc-number").validity.customError,
+ false,
+ "cc-number field should not have a custom error"
+ );
+ EventUtils.synthesizeKey("4111111111111112", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ is(
+ win.document.querySelector("#cc-number").validity.customError,
+ true,
+ "cc-number field should have a custom error"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ win.document.querySelector("#cancel").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_noAutocompletePopupOnSystemTab() {
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PRIVACY_PREF_URL },
+ async browser => {
+ // Open credit card manage dialog
+ await SpecialPowers.spawn(browser, [], async () => {
+ let button = content.document.querySelector(
+ "#creditCardAutofill button"
+ );
+ button.click();
+ });
+ let dialog = await waitForSubDialogLoad(
+ content,
+ MANAGE_CREDIT_CARDS_DIALOG_URL
+ );
+
+ // Open edit credit card dialog
+ await SpecialPowers.spawn(dialog, [], async () => {
+ let button = content.document.querySelector("#add");
+ button.click();
+ });
+ dialog = await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL);
+
+ // Focus on credit card number field
+ await SpecialPowers.spawn(dialog, [], async () => {
+ let number = content.document.querySelector("#cc-number");
+ number.focus();
+ });
+
+ // autocomplete popup should not appear
+ await ensureNoAutocompletePopup(browser);
+ }
+ );
+
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js
new file mode 100644
index 0000000000..5de499b942
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js
@@ -0,0 +1,145 @@
+"use strict";
+
+// Remove the scheme from the URLs so we can switch between http: and https: later.
+const TEST_URL_PATH_CC =
+ "://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/autocomplete_creditcard_basic.html";
+const TEST_URL_PATH =
+ "://example.org" + HTTP_TEST_PATH + "autocomplete_basic.html";
+
+add_task(async function setup_storage() {
+ await setStorage(
+ TEST_ADDRESS_1,
+ TEST_ADDRESS_2,
+ TEST_ADDRESS_3,
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3
+ );
+});
+
+add_task(async function test_insecure_form() {
+ async function runTest({
+ urlPath,
+ protocol,
+ focusInput,
+ expectedType,
+ expectedResultLength,
+ }) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: protocol + urlPath },
+ async function (browser) {
+ await openPopupOn(browser, focusInput);
+
+ const items = getDisplayedPopupItems(browser);
+ is(
+ items.length,
+ expectedResultLength,
+ `Should show correct amount of results in "${protocol}"`
+ );
+ const firstItem = items[0];
+ is(
+ firstItem.getAttribute("originaltype"),
+ expectedType,
+ `Item should attach with correct binding in "${protocol}"`
+ );
+
+ await closePopup(browser);
+ }
+ );
+ }
+
+ const testSets = [
+ {
+ urlPath: TEST_URL_PATH,
+ protocol: "https",
+ focusInput: "#organization",
+ expectedType: "autofill-profile",
+ expectedResultLength: 2,
+ },
+ {
+ urlPath: TEST_URL_PATH,
+ protocol: "http",
+ focusInput: "#organization",
+ expectedType: "autofill-profile",
+ expectedResultLength: 2,
+ },
+ {
+ urlPath: TEST_URL_PATH_CC,
+ protocol: "https",
+ focusInput: "#cc-name",
+ expectedType: "autofill-profile",
+ expectedResultLength: 3,
+ },
+ {
+ urlPath: TEST_URL_PATH_CC,
+ protocol: "http",
+ focusInput: "#cc-name",
+ expectedType: "autofill-insecureWarning", // insecure warning field
+ expectedResultLength: 1,
+ },
+ ];
+
+ for (const test of testSets) {
+ await runTest(test);
+ }
+});
+
+add_task(async function test_click_on_insecure_warning() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http" + TEST_URL_PATH_CC },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-name");
+ const insecureItem = getDisplayedPopupItems(browser)[0];
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+ await EventUtils.synthesizeMouseAtCenter(insecureItem, {});
+ // Check input's value after popup closed to ensure the completion of autofilling.
+ await popupClosePromise;
+
+ const inputValue = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ return content.document.querySelector("#cc-name").value;
+ }
+ );
+ is(inputValue, "");
+
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_press_enter_on_insecure_warning() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http" + TEST_URL_PATH_CC },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-name");
+
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ // Check input's value after popup closed to ensure the completion of autofilling.
+ await popupClosePromise;
+
+ const inputValue = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ return content.document.querySelector("#cc-name").value;
+ }
+ );
+ is(inputValue, "");
+
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js b/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js
new file mode 100644
index 0000000000..7f54140c78
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js
@@ -0,0 +1,290 @@
+"use strict";
+
+const TEST_SELECTORS = {
+ selRecords: "#credit-cards",
+ btnRemove: "#remove",
+ btnAdd: "#add",
+ btnEdit: "#edit",
+};
+
+const DIALOG_SIZE = "width=600,height=400";
+
+add_task(async function test_manageCreditCardsInitialState() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [TEST_SELECTORS], args => {
+ let selRecords = content.document.querySelector(args.selRecords);
+ let btnRemove = content.document.querySelector(args.btnRemove);
+ let btnAdd = content.document.querySelector(args.btnAdd);
+ let btnEdit = content.document.querySelector(args.btnEdit);
+
+ is(selRecords.length, 0, "No credit card");
+ is(btnRemove.disabled, true, "Remove button disabled");
+ is(btnAdd.disabled, false, "Add button enabled");
+ is(btnEdit.disabled, true, "Edit button disabled");
+ });
+ }
+ );
+});
+
+add_task(async function test_cancelManageCreditCardsDialogWithESC() {
+ let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL);
+ await waitForFocusAndFormReady(win);
+ let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ await unloadPromise;
+ ok(true, "Manage credit cards dialog is closed with ESC key");
+});
+
+add_task(async function test_removingSingleAndMultipleCreditCards() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.reduceTimerPrecision", false]],
+ });
+ await setStorage(
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3,
+ TEST_CREDIT_CARD_4,
+ TEST_CREDIT_CARD_5
+ );
+
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+ let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
+ let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+
+ const expectedLabels = [
+ {
+ id: "credit-card-label-number-name-2",
+ args: { number: "**** 1881", name: "Chris P. Bacon", type: "Visa" },
+ },
+ {
+ id: "credit-card-label-number-2",
+ args: { number: "**** 5100", type: "MasterCard" },
+ },
+ {
+ id: "credit-card-label-number-expiration-2",
+ args: {
+ number: "**** 7870",
+ month: "1",
+ year: "2000",
+ type: "MasterCard",
+ },
+ },
+ {
+ id: "credit-card-label-number-name-expiration-2",
+ args: {
+ number: "**** 1045",
+ name: "Timothy Berners-Lee",
+ month: "12",
+ year: (new Date().getFullYear() + 10).toString(),
+ type: "Visa",
+ },
+ },
+ {
+ id: "credit-card-label-number-name-expiration-2",
+ args: {
+ number: "**** 1111",
+ name: "John Doe",
+ month: "4",
+ year: new Date().getFullYear().toString(),
+ type: "Visa",
+ },
+ },
+ ];
+
+ is(
+ selRecords.length,
+ expectedLabels.length,
+ "Correct number of credit cards"
+ );
+ expectedLabels.forEach((expected, i) => {
+ const l10nAttrs = document.l10n.getAttributes(selRecords[i]);
+ is(
+ l10nAttrs.id,
+ expected.id,
+ `l10n id set for credit card ${expectedLabels.length - i}`
+ );
+ Object.keys(expected.args).forEach(arg => {
+ is(
+ l10nAttrs.args[arg],
+ expected.args[arg],
+ `Set display ${arg} for credit card ${expectedLabels.length - i}`
+ );
+ });
+ });
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ is(btnRemove.disabled, false, "Remove button enabled");
+ is(btnEdit.disabled, false, "Edit button enabled");
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 4, "Four credit cards left");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[3],
+ { shiftKey: true },
+ win
+ );
+ is(btnEdit.disabled, true, "Edit button disabled when multi-select");
+
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "All credit cards are removed");
+
+ win.close();
+});
+
+add_task(async function test_removingCreditCardsViaKeyboardDelete() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ is(selRecords.length, 1, "One credit card");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "No credit cards left");
+
+ win.close();
+});
+
+add_task(async function test_creditCardsDialogWatchesStorageChanges() {
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 1, "One credit card is shown");
+
+ await removeCreditCards([selRecords.options[0].value]);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 0, "Credit card is removed");
+ win.close();
+});
+
+add_task(async function test_showCreditCardIcons() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.reduceTimerPrecision", false]],
+ });
+ await setStorage(TEST_CREDIT_CARD_1);
+ await setStorage(TEST_CREDIT_CARD_3);
+
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ is(
+ selRecords.classList.contains("branded"),
+ AppConstants.MOZILLA_OFFICIAL,
+ "record picker has 'branded' class in an MOZILLA_OFFICIAL build"
+ );
+
+ let option0 = selRecords.options[0];
+ let icon0Url = win.getComputedStyle(option0, "::before").backgroundImage;
+ let option1 = selRecords.options[1];
+ let icon1Url = win.getComputedStyle(option1, "::before").backgroundImage;
+
+ is(
+ option0.getAttribute("cc-type"),
+ "mastercard",
+ "Option has the expected cc-type"
+ );
+ is(
+ option1.getAttribute("cc-type"),
+ "visa",
+ "Option has the expected cc-type"
+ );
+
+ if (AppConstants.MOZILLA_OFFICIAL) {
+ ok(
+ icon0Url.includes("icon-credit-card-generic.svg"),
+ "unknown network option ::before element has the generic icon as backgroundImage: " +
+ icon0Url
+ );
+ ok(
+ icon1Url.includes("cc-logo-visa.svg"),
+ "visa option ::before element has the visa icon as backgroundImage " +
+ icon1Url
+ );
+ }
+
+ await removeCreditCards([option0.value, option1.value]);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 0, "Credit card is removed");
+ win.close();
+});
+
+add_task(async function test_hasEditLoginPrompt() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+ let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
+ let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
+ let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
+ EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
+ await osKeyStoreLoginShown;
+ await new Promise(resolve => waitForFocus(resolve, win));
+ await new Promise(resolve => executeSoon(resolve));
+
+ // Login is not required for removing credit cards.
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "Credit card is removed");
+
+ // gSubDialog.open should be called when trying to add a credit card,
+ // no OS login dialog is required.
+ window.gSubDialog = {
+ open: url =>
+ is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"),
+ };
+ EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win);
+ delete window.gSubDialog;
+
+ win.close();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/head_cc.js b/browser/extensions/formautofill/test/browser/creditCard/head_cc.js
new file mode 100644
index 0000000000..42196e8422
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/head_cc.js
@@ -0,0 +1 @@
+/* import-globals-from ../head.js */
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Empty file</title>
+</head>
+<body>
+</body>
+</html>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <path fill="#CCC" fill-rule="evenodd" d="M30 5V1a1 1 0 0 0-1-1H1a1 1 0 0 0-1 1v4h30zm0 4v10a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V9h30z"/>
+</svg>
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 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="348.333px" height="348.333px" viewBox="0 0 348.333 348.334" style="enable-background:new 0 0 348.333 348.334;" xml:space="preserve"><g><path fill="#565656" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z"/></g></svg> \ 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
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png
Binary files 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
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif
Binary files 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="164.105px" height="48.177px" viewBox="0 21.835 164.105 48.177" enable-background="new 0 21.835 164.105 48.177"
+ xml:space="preserve">
+<path fill="#78BE20" d="M134.879,52.052l4.932-16.246l4.919,16.246H134.879z M98.851,57.406h-3.028V34.901h2.989
+ c4.53,0,9.571,1.875,9.571,11.2C108.383,54.361,103.918,57.406,98.851,57.406 M18.927,52.052l5.033-16.436l4.94,16.436H18.927z
+ M148.76,23.221h-16.905l-9.64,27.871c0.427-2.314,0.504-4.164,0.504-4.968c0-11.677-6.697-22.902-22.437-22.902H82.209
+ l0.056,27.724c-1.565-7.876-8.77-9.987-17.731-12.708c-3.348-1.03-5.186-2.666-4.516-4.193c0.574-1.332,2.626-1.75,5.118-1.405
+ c3.802,0.543,6.838,1.813,9.763,3.401l3.995-9.885c-0.902-0.442-7.374-4.323-15.485-4.323c-11.311,0-18.535,5.511-18.535,13.644
+ c0,7.249,4.469,11.462,12.686,13.823c8.839,2.562,11.079,3.589,10.814,6.13c-0.231,2.187-5.708,4.853-19.11-3.384l-3.982,8.23
+ L32.989,23.221h-16.9L0.115,69.285h13.321l2.078-6.524h16.797l1.987,6.524h13.985l-1.585-4.507
+ c4.729,2.73,10.591,5.228,17.604,5.228c10.747,0,16.555-5.86,17.939-11.477v10.756h18.025c10.74,0,16.361-5.118,19.298-10.591
+ l-3.687,10.591h13.312l2.162-6.524h16.781l1.931,6.524h14.001L148.76,23.221z"/>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
+ <defs>
+ <path id="a" d="M11.5 17a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1zm1.003-3h-1a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5zm-9.218 5h17.43a.5.5 0 0 0 .436-.746l-8.716-15.42a.5.5 0 0 0-.87 0l-8.716 15.42a.5.5 0 0 0 .436.746zM13.74 1.08l9.572 16.936A2 2 0 0 1 21.573 21H2.427a2 2 0 0 1-1.741-2.984L10.259 1.08a2 2 0 0 1 3.482 0z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd" transform="translate(4 6)">
+ <mask id="b" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <use fill="#d43030" xlink:href="#a"/>
+ <g fill="#d43030" mask="url(#b)">
+ <path d="M-4-6h32v32H-4z"/>
+ </g>
+ </g>
+</svg>
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 @@
+<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M17 19.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V17h-2.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H15v-2.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V15h2.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H17zm-7.611 4.504L12.394 22H25.5a.5.5 0 00.5-.5v-11a.5.5 0 00-.5-.5h-19a.5.5 0 00-.5.5v11a.5.5 0 00.5.5H9v1.796a.25.25 0 00.389.208zM7 27.066V24H6a2 2 0 01-2-2V10a2 2 0 012-2h20a2 2 0 012 2v12a2 2 0 01-2 2H13l-5.223 3.482A.5.5 0 017 27.066z" fill="#3d3d3d"/></svg>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="111px" height="33px" viewBox="0 0 111 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>ASDA Logo - White</title>
+ <g id="Footer-Amends" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" transform="translate(-1.000000, -1.000000)" fill="#FFFFFF">
+ <g id="Logo" transform="translate(1.000000, 1.000000)">
+ <path d="M100.697948,1.012292 L89.3366436,1.012292 L82.8584522,19.7642556 C83.148104,18.2068119 83.1992581,16.9618991 83.1992581,16.4207628 C83.1992581,8.56536178 78.6975861,1.012292 68.1226373,1.012292 L55.9778554,1.012292 L56.013619,19.6652524 C54.9634097,14.3658639 50.1220389,12.9452287 44.1009128,11.1157202 C41.8504643,10.4218107 40.6150151,9.3208018 41.0659906,8.29407301 C41.4513073,7.398942 42.8306965,7.11767529 44.5051631,7.34860889 C47.0601003,7.71502058 49.0999533,8.56990728 51.0644038,9.63765645 L53.7484443,2.98659136 C53.1426778,2.68836218 48.7941359,0.0782470703 43.3442306,0.0782470703 C35.744192,0.0782470703 30.8900881,3.78626686 30.8900881,9.25771912 C30.8900881,14.1342651 33.8930113,16.9692162 39.4152188,18.5582567 C45.353524,20.2807797 46.8595806,20.9735805 46.6813163,22.6821344 C46.5266361,24.1539896 42.8459763,25.947245 33.8399749,20.4062798 L31.1644602,25.9433647 L22.9041793,1.012292 L11.5474142,1.012292 L0.81413124,32.0042908 L9.76565687,32.0042908 L11.5,27 L22,27 L23.7839856,32.0042908 L33.1815042,32.0042908 L32.1156829,28.9725528 C35.293438,30.8094893 39.2329685,32.4896615 43.945236,32.4896615 C51.1667121,32.4896615 55.0690396,28.5473822 56,24.7678539 L56,32.0042908 L68.1107899,32.0042908 C75.3288336,32.0042908 79.1053795,28.561573 81.0783558,24.878498 L78.600814,32.0042908 L87.5461392,32.0042908 L89.3366436,27 L100,27 L101.571997,32.0042908 L110.981142,32.0042908 L100.697948,1.012292 Z M13.5,20 L16.8375459,9.35239857 L20,20 L13.5,20 Z M65,24.0118596 L65,9 L67.1348759,9 C70.178213,9 73.5,10.2318086 73.5,16.5059298 C73.5,22.063414 70.5657441,24.0118596 67.1610065,24.0118596 L65,24.0118596 Z M91.5,20 L94.6839085,9.47878566 L98,20 L91.5,20 Z" id="Fill-3"></path>
+ </g>
+ </g>
+ </g>
+</svg>
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
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin
Binary files 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 @@
+<svg width="136" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path d="M79.039 7.346c0 1.784-.449 3.186-1.346 4.206-.897 1.021-2.152 1.532-3.767 1.532-1.641 0-2.905-.505-3.791-1.513-.887-1.008-1.335-2.422-1.346-4.24 0-1.815.449-3.221 1.346-4.22.897-1 2.165-1.498 3.805-1.496 1.6 0 2.85.507 3.748 1.523.899 1.015 1.35 2.418 1.351 4.208zm-8.88 0c0 1.51.32 2.654.963 3.434.642.78 1.577 1.17 2.804 1.168 1.234 0 2.166-.388 2.796-1.165.63-.777.945-1.923.947-3.437 0-1.498-.314-2.634-.942-3.41-.627-.774-1.557-1.163-2.787-1.164-1.235 0-2.173.39-2.815 1.17-.642.78-.964 1.915-.964 3.404h-.002zm16.891 5.587V7.535c0-.68-.155-1.188-.466-1.523-.31-.336-.795-.504-1.455-.504-.874 0-1.514.236-1.922.708-.407.472-.61 1.251-.61 2.339v4.378h-1.265V4.575h1.028l.204 1.143h.062a2.583 2.583 0 011.076-.955 3.541 3.541 0 011.564-.339c1.006 0 1.763.242 2.271.727.508.484.762 1.26.762 2.327v5.455H87.05zm7.392.151c-1.234 0-2.208-.376-2.922-1.128-.714-.752-1.073-1.796-1.077-3.132 0-1.346.332-2.415.996-3.208a3.302 3.302 0 012.672-1.19 3.151 3.151 0 012.484 1.034c.61.689.915 1.597.915 2.723v.808h-5.75c.024.98.272 1.725.742 2.233a2.57 2.57 0 001.986.762 6.727 6.727 0 002.667-.566v1.128c-.409.18-.834.319-1.27.414-.476.09-.96.13-1.443.122zm-.344-7.597c-.609-.03-1.2.21-1.615.656a3.022 3.022 0 00-.705 1.814h4.378c0-.798-.18-1.409-.538-1.832a1.884 1.884 0 00-1.52-.638zm9.192 7.446h-1.294V2.94h-3.53V1.79h8.341v1.152h-3.517zm8.824-8.506c.335-.004.67.027.998.091l-.175 1.173c-.3-.07-.607-.108-.915-.113a2.228 2.228 0 00-1.733.824c-.488.57-.745 1.3-.72 2.05v4.48h-1.266V4.576h1.044l.146 1.547h.062c.27-.5.654-.93 1.119-1.257a2.521 2.521 0 011.44-.438zm3.748.148v5.422c0 .682.155 1.19.466 1.523.31.334.795.501 1.456.503.873 0 1.512-.238 1.916-.716.403-.477.605-1.256.605-2.338V4.575h1.265v8.342h-1.044l-.183-1.12h-.068c-.26.412-.633.74-1.076.945a3.627 3.627 0 01-1.574.328c-1.016 0-1.776-.241-2.282-.724-.506-.482-.759-1.255-.759-2.317V4.575h1.278zm13.781 6.079a2.09 2.09 0 01-.87 1.797c-.579.422-1.392.633-2.437.633-1.107 0-1.971-.18-2.592-.539v-1.16c.412.207.845.368 1.292.48.434.113.88.172 1.33.174.526.03 1.051-.08 1.522-.317a1.076 1.076 0 00.11-1.798 6.663 6.663 0 00-1.649-.807 8.931 8.931 0 01-1.658-.775 2.258 2.258 0 01-.737-.73 1.923 1.923 0 01-.24-.981 1.884 1.884 0 01.832-1.615c.554-.393 1.314-.59 2.28-.59a6.668 6.668 0 012.636.539l-.449 1.028a6.052 6.052 0 00-2.28-.52 2.624 2.624 0 00-1.345.27.872.872 0 00-.457.777.947.947 0 00.172.57c.15.188.338.341.552.45.474.237.963.443 1.464.616.99.36 1.659.718 2.007 1.077.36.383.546.896.517 1.42zm4.755 1.386c.217 0 .434-.016.648-.049.167-.024.332-.058.495-.102v.968a2.312 2.312 0 01-.605.165c-.238.04-.48.062-.721.064-1.615 0-2.422-.851-2.422-2.554v-4.97h-1.198v-.61l1.198-.539.538-1.784h.732v1.946h2.422v.982h-2.422v4.922c-.028.416.1.829.358 1.157.25.273.607.42.977.403z" fill="#6FBE4A"/><text transform="translate(.79 .615)" fill="#696969" font-family="ArialMT, Arial" font-size="12.109"><tspan x=".034" y="11">Powered by</tspan></text></g></svg> \ 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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="30" height="20" fill="#353A48" rx="1"/>
+ <circle cx="11" cy="10" r="6" fill="#ED0006"/>
+ <circle cx="20" cy="10" r="6" fill="#F9A000"/>
+ <path fill="#FF8150" d="M15.5 6a5.731 5.731 0 0 1 1.597 4 5.731 5.731 0 0 1-1.597 4 5.731 5.731 0 0 1-1.597-4c0-1.567.612-2.983 1.597-4z"/>
+ </g>
+</svg>
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 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 22"><defs><style>.cls-1{fill:#191f70;}.cls-2{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1-2"><path class="cls-1" d="M.91,0H31.09A1,1,0,0,1,32,1V21a1,1,0,0,1-.91,1H.91A1,1,0,0,1,0,21V1A1,1,0,0,1,.91,0Z"/><path class="cls-2" d="M16.14,9.52c0,1.23,1.09,1.91,1.92,2.32s1.15.68,1.15,1.05c0,.58-.69.83-1.32.84a4.55,4.55,0,0,1-2.26-.54l-.4,1.87a6.82,6.82,0,0,0,2.45.45c2.31,0,3.82-1.15,3.83-2.91,0-2.24-3.1-2.38-3.08-3.38,0-.31.29-.63.93-.71a4.07,4.07,0,0,1,2.17.37l.39-1.8a6,6,0,0,0-2.06-.37c-2.17,0-3.7,1.16-3.73,2.81m9.51-2.66a1,1,0,0,0-.94.63l-3.3,7.89h2.31l.46-1.27H27l.26,1.27h2L27.52,6.86ZM26,9.17l.67,3.19H24.81ZM13.33,6.86l-1.81,8.51h2.2l1.82-8.51Zm-3.25,0-2.29,5.8L6.86,7.73a1,1,0,0,0-1-.87H2.09l0,.25a9.22,9.22,0,0,1,2.17.72.93.93,0,0,1,.53.75l1.75,6.79H8.82l3.57-8.51Z"/></g></g></svg> \ 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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="109" viewBox="0 0 24 109">
+ <g fill="none" fill-rule="nonzero">
+ <rect width="24" height="109" fill="#3D3D3D" rx="12"/>
+ <path stroke="#EEE" stroke-linecap="square" stroke-width="2" d="M5.5 50.5h13M5.5 55.25h13M5.5 60.25h13"/>
+ </g>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="30" height="20" fill="#007ECD" rx="1"/>
+ <path fill="#FFF" d="M9.72 13H8.47l-.498-1.363H5.69L5.222 13H4l2.22-6h1.218l2.283 6zm-2.119-2.374L6.816 8.4l-.77 2.226H7.6zM10.316 13V7h1.723l1.034 4.093L14.096 7h1.727v6h-1.07V8.277L13.622 13h-1.109l-1.128-4.723V13h-1.07zm6.65 0V7h4.228v1.015h-3.077v1.33h2.863v1.011h-2.863v1.633h3.186V13h-4.337zm4.733 0l1.949-3.131L21.882 7h1.346l1.143 1.928L25.491 7h1.334l-1.773 2.914L27 13h-1.388l-1.264-2.075L23.08 13H21.7z"/>
+ </g>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="30" height="20" fill="#000052" rx="1"/>
+ <circle cx="11" cy="10" r="6" fill="#06C"/>
+ <circle cx="20" cy="10" r="6" fill="#C00"/>
+ <path fill="#C70B8C" d="M15.5 6a5.731 5.731 0 0 1 1.597 4 5.731 5.731 0 0 1-1.597 4 5.731 5.731 0 0 1-1.597-4c0-1.567.612-2.983 1.597-4z"/>
+ </g>
+</svg>
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
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2
Binary files 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
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2
Binary files 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
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2
Binary files 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 @@
+<html lang="en-GB"><!--
+ Page saved with SingleFile
+ url: https://www.asda.com/account?request_origin=asda&redirect_uri=https%3A%2F%2Fwww.asda.com%2Fgood-living%2Ftag%2Frecipes
+ saved date: Wed Mar 16 2022 11:21:53 GMT+0200 (Eastern European Standard Time)
+--><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,shrink-to-fit=no"><meta name="referrer" content="no-referrer-when-downgrade"><title>My Account | Account Settings – Asda Groceries</title><style>:root{--sf-img-16: url("resources/sample/1.svg");--sf-img-19: url("resources/sample/2.svg");--sf-img-20: url("resources/sample/3.svg");--sf-img-15: url("resources/sample/4.svg");--sf-img-17: url("resources/sample/5.svg");--sf-img-18: url("resources/sample/6.svg")}</style><style>html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}article *,article *:before,article *:after{box-sizing:border-box}@font-face{font-family:"SourceSansProBold";src:url(resources/sample/7.woff2) format("woff2")}@font-face{font-family:"SourceSansProSemiBold";src:url(resources/sample/8.woff2) format("woff2")}@font-face{font-family:"SourceSansProRegular";src:url(resources/sample/9.woff2) format("woff2")}h1,h2,h3,h4,p,a{color:#191919;padding-bottom:8px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1{font-size:1.25em;font-weight:700;color:#191919;padding-bottom:16px;line-height:1.25}.theme-ghs h1{font-family:SourceSansProBold,sans-serif}.theme-george h1{font-family:LatoBold,sans-serif}h2{font-size:1.125em;font-weight:800}.theme-ghs h2{font-family:SourceSansProBold,sans-serif}.theme-george h2{font-family:LatoBold,sans-serif}h3{font-size:1em;font-weight:600}.theme-ghs h3{font-family:SourceSansProSemiBold,sans-serif}.theme-george h3{font-family:LatoRegular,sans-serif}h4{font-size:.875em;font-weight:400}.theme-ghs h4{font-family:SourceSansProRegular,sans-serif}.theme-george h4{font-family:LatoRegular,sans-serif}p{color:off-black;padding-bottom:16px;font-size:.875em;letter-spacing:.2px;line-height:1.3}.theme-ghs p{font-family:SourceSansProRegular,sans-serif}.theme-george p{font-family:LatoRegular,sans-serif}a{margin-bottom:16px;padding-bottom:0;font-weight:500;font-size:1em}.theme-ghs a{color:#0073b1;text-decoration:none;font-family:SourceSansProBold,sans-serif}.theme-george a{color:#191919;text-decoration:underline;font-family:LatoRegular,sans-serif}.button-as-link{display:inline;background-color:transparent;border:0;padding:0;width:auto!important;height:auto!important;color:#0073b1;margin-left:0;padding-bottom:16px;font-weight:500;text-decoration:none}.theme-ghs .button-as-link{font-size:.875em}.theme-george .button-as-link{font-size:12px}.theme-ghs .button-as-link{color:#0073b1}.theme-george .button-as-link{color:#191919}.short-password-error{bottom:62px}.blank-password-error{bottom:50px}a:hover{cursor:pointer}.theme-ghs strong{font-family:SourceSansProBold,sans-serif}.theme-george strong{font-family:LatoBold,sans-serif}.input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;line-height:1.125em}.theme-ghs .input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;clear:both}.theme-george .input-error{font-family:LatoBold,sans-serif;font-size:.875em;color:#da291c;clear:both}.theme-ghs .input-error a{font-family:SourceSansProRegular,sans-serif;color:#d43030;text-decoration:underline}.theme-george .input-error a{font-family:LatoBold,sans-serif;color:#da291c;text-decoration:underline}.left{float:left!important}.right{float:right!important;text-align:right!important}.theme-ghs .right{padding-top:3px;line-height:1.5}.theme-george .right{line-height:1.5}.center{text-align:center!important}.red{color:#da0500!important}.orange{color:#fdb45b!important}.pink{color:#ec938e!important}.yellow{color:#fae100!important}.green{color:#68a51c!important}.blue{color:#0073b1!important}.grey{color:grey1!important}.white{color:#fff!important}.black{color:#191919!important}.regular{font-weight:400!important}.bold{font-weight:700!important}@media (min-width:768px){.main-container.login{margin-top:32px}}form{border-bottom:1px solid #ccc;padding:8px 0 16px;margin-bottom:16px}form label.for-username{padding-bottom:4px;display:block}.theme-ghs form label.for-username{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em;padding:0 0 4px}.theme-george form label.for-username{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px;padding:0 0 8px}form .username-box.error{height:74px}form .username-box .alert{top:4px}form .login-links{width:100%;height:30px;padding:10px 0;margin-bottom:10px}form .login-links a{font-size:14px}form .form-error{margin-bottom:20px;margin-top:5px;font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#da291c;line-height:16px;clear:both}form .form-error a{color:#da291c;font-family:SourceSansProRegular,sans-serif;text-decoration:underline}form .dropdown-wrapper.phone-login-dropdown{float:none}form .dropdown-wrapper.phone-login-dropdown .dropdown-list::-webkit-scrollbar-thumb{height:150px}form .dropdown-wrapper{width:100%}form .dropdown-wrapper .list-wrapper .dropdown-list{width:498px;border:solid 1px #ccc;height:192px;padding:20px 20px}form .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{display:flex;font-size:14px;font-weight:normal;padding-bottom:12px;justify-content:start}form .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:auto;text-align:left;padding-right:15px}form .dropdown-header.phone-login-dropdown-header{height:44px;width:unset;border-radius:4px;border:solid 1px #ccc;margin-bottom:10px}form .dropdown-header.phone-login-dropdown-header .country-code{flex-grow:1;font-weight:normal;font-size:14px;margin-left:10px}form .remember-me{bottom:38px;width:130px}.theme-ghs form .remember-me{text-decoration:none}.theme-george form .remember-me{text-decoration:underline}a{user-select:none}a.login-link{color:#da291c;text-decoration:underline}.login-container .softLogin-form{border-bottom:0;margin:0;padding:8px 0 0}.login-container button.reset-pwd{border:0;font-size:14px;padding:0px;height:auto;width:auto;margin:0 0 16px 0;letter-spacing:inherit;text-transform:inherit;-webkit-font-smoothing:antialiased}.theme-ghs .login-container button.reset-pwd{color:#0073b1;text-decoration:none;font-family:SourceSansProBold,sans-serif}.theme-george .login-container button.reset-pwd{color:#191919;text-decoration:underline;font-family:LatoRegular,sans-serif}.reset-password-container .code-link{display:inline-block;padding-bottom:0;margin-top:16px}.reset-password-container button.basic{margin-top:20px}.reset-password-container form{border-bottom:0;margin-bottom:0}.reset-password-container .input-box input::placeholder{font-size:14px;color:#767676}.reset-code form{padding-bottom:0}.reset-code .input-box input.half,.reset-code button{width:48%;margin-right:0}.reset-code .reset-code-container{position:relative;padding-bottom:27px}.reset-code .reset-code-container .input-error{position:absolute;top:57px}.reset-code .reset-code-container .alert{top:20px;left:38%}@media (min-width:768px){.reset-code .reset-code-container .alert{left:44%}}.reset-code .code-label{display:inline;padding-bottom:0}.req-new-code{margin-top:8px;margin-bottom:8px;display:inline-block}.theme-ghs .req-new-code{font-family:SourceSansProBold,sans-serif;text-decoration:none;padding:0;text-transform:none}.theme-george .req-new-code{font-family:LatoBold,sans-serif;text-decoration:underline;font-size:14px;padding:0;text-transform:none}.try-other-email{font-size:1em}@media (min-width:768px){.reset-code .input-box input.half{width:54%}.reset-code button{width:44%}}.recaptcha-container{border-bottom:1px #ccc solid;margin-bottom:20px}.recaptcha-container .g-recaptcha{margin:10px 0 20px 0}.reset-confirmation{clear:fix();height:240px}.reset-confirmation button.full{margin-bottom:16px}.reset-confirmation button.full{width:100%;margin:0 0 16px;float:left}.theme-ghs .reset-confirmation button.next-dest a{color:#68a51c}.theme-george .reset-confirmation button.next-dest a{color:#191919}.password-strength{clear:both;font-family:SourceSansProRegular,sans-serif}.password-strength .strength-label{font-family:SourceSansProSemiBold,sans-serif;font-size:.875em}.password-strength .password-strength-measure{color:#767676;font-size:.875em;margin-top:5px;display:flex;justify-content:space-between}.theme-ghs .password-strength .password-strength-measure{font-family:SourceSansProRegular,sans-serif}.theme-george .password-strength .password-strength-measure{font-family:LatoRegular,sans-serif}.password-strength .strength-text{font-size:.875em}.password-strength .strength-indicator{display:inline-block;height:5px;border-radius:5px;position:relative;bottom:5px}.password-strength .strength-indicator.invalid{background-color:#c2c2c2;width:10%}.password-strength .strength-indicator.weak{background:#d43030;width:20%}.password-strength .strength-indicator.fair{background:#fbc42c;width:50%}.password-strength .strength-indicator.strong{background:#68a51c;width:100%}.password-strength .strength-indicator-bar{background:#e9e9e9;width:100%;height:5px;border-radius:5px}.password-strength .strength-desc{margin-top:16px}.password-strength .strength-desc button{font-size:.875em;display:inline;background-color:transparent;border:0;padding:0;color:#0073b1;margin-left:0;text-transform:none;letter-spacing:normal;margin-bottom:16px;font-weight:500}.theme-ghs .password-strength .strength-desc button{color:#0073b1;letter-spacing:.5px}.theme-george .password-strength .strength-desc button{color:#191919;letter-spacing:.5px}.password-strength .strength-desc button .chevron{padding-left:6px}.password-strength .strength-desc button .chevron.flipped{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg);padding-top:0;padding-bottom:0;padding-left:0;padding-right:6px}.password-strength .strength-desc .strength-para{margin-bottom:16px}.password-strength .strength-desc .strength-para p{padding:0}.password-strength .strength-desc .strength-para ul{list-style-type:unset;list-style-position:inside;margin-top:8px}.password-strength .strength-desc .strength-para ul li{font-family:SourceSansProRegular,sans-serif;font-size:.875em;margin-left:8px;margin-bottom:4px}.postcode-wrap{width:100%;float:left;margin-bottom:8px}.postcode-wrap .custom-input.half{width:50%;text-transform:uppercase}.postcode-wrap .custom-input.half+.input-error{clear:both;line-height:17px;padding-top:5px}.regCheckTnC .register-link{font-size:100%}.theme-ghs .regCheckTnC .register-link{font-family:SourceSansProBold,sans-serif}.theme-george .regCheckTnC .register-link{font-family:LatoBold,sans-serif}.regCheckTnC .input-error{font-size:.875em}.register-container form{border-bottom:0;padding-bottom:0;margin-bottom:0}.register-container form button+p{padding-top:20px;padding-bottom:0}.register-container .register-link{font-size:100%}.theme-ghs .register-container .register-link{font-family:SourceSansProBold,sans-serif}.theme-george .register-container .register-link{font-family:LatoBold,sans-serif}.register-container .george-reward-link{font-size:14px;font-family:LatoBold,sans-serif}.password-container{position:relative}.password-container .button-show-password{position:absolute;padding-bottom:0;text-decoration:none;right:12px;text-align:center;display:inline-block;margin:0;width:40px!important;transition:all 200ms ease;padding:0}.theme-ghs .password-container .button-show-password{font-family:SourceSansProSemiBold,sans-serif;top:30px;text-transform:none;font-size:1em}.theme-george .password-container .button-show-password{font-family:LatoRegular,sans-serif;top:37px;text-transform:uppercase;font-size:.75em}.password-container .input-box input.show-password{padding-right:60px}.password-container .show-password{padding-right:65px;margin-bottom:7px}.co-third-party-login__container .primary{margin-bottom:16px}.co-third-party-login__container h3{margin:7.2px 0 24px 0}.co-login-disclaimer p{padding:16px 0 0}.co-login-disclaimer a{font-family:SourceSansProRegular;font-weight:bold;font-size:.95em}form#cd-reg-form{border-bottom:0;padding-bottom:0;margin-bottom:0}form#cd-reg-form .form-error{margin-bottom:20px;margin-top:5px;font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#da291c;line-height:16px;clear:both}form#cd-reg-form input:last-child{margin-bottom:16px}a{user-select:none}a.login-link{color:#da291c;text-decoration:underline}.login-container .softLogin-form{border-bottom:0;margin:0;padding:8px 0 0}.page-mask{position:fixed;width:100vw;height:100vh;left:0;top:0;background-color:#000;opacity:.5}.cd-modal{position:absolute;z-index:1000;top:10vh;left:calc(50% - 200px);width:400px;height:auto;border-radius:2px;background-color:#fff;box-shadow:0 5px 10px 0 rgba(0,0,0,.2)}.cd-modal h1{padding:20px;border-bottom:1px solid #ccc}.cd-modal p{padding:20px 40px 20px 20px;border-bottom:1px solid #ccc}.theme-george .register-container a.tc-link-register{font-family:LatoBold,sans-serif}.theme-george .register-container .password-strength .strength-desc button{text-decoration:underline}.app{display:flex;flex-wrap:wrap;align-items:flex-start;flex-direction:row}.container-wrapper{height:auto;flex-basis:calc((100% * 1) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0 + (16px / 2))!important;min-height:calc(100vh - 200px)}.theme-ghs .container-wrapper{min-height:calc(100vh - 300px)}.theme-george .container-wrapper{min-height:calc(100vh - 261px)}@media (min-width:481px){.container-wrapper{flex-basis:calc((100% * 0.6666666667) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0.1666666667 + (16px / 2))!important;min-height:calc(100vh - 200px)}}@media (min-width:768px){.container-wrapper{flex-basis:calc((100% * 0.5) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0.25 + (16px / 2))!important}}@media (min-width:961px){.container-wrapper{flex-basis:calc((100% * 0.3333333333) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0.3333333333 + (16px / 2))!important}}.main-container{height:auto;padding:4px;max-width:432px;margin:0 auto}@media (min-width:481px){.main-container{border:1px solid #ccc}.theme-ghs .main-container{padding:20px;box-shadow:0 0}.theme-george .main-container{padding:32px;box-shadow:4px 4px 4px 0 rgba(0,0,0,.06)}}footer{width:100%;margin-top:60px}.theme-ghs footer{background-color:#191919;min-height:100px}.theme-george footer{background-color:#fff;min-height:60px}.theme-ghs footer{border-top:0}.theme-george footer{border-top:1px solid #cbcbcb}footer .up-arrow{margin-left:5px;-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg);height:10px;width:10px}.footer-wrapper{position:relative;max-width:1024px;margin:0 auto;padding:16px}.footer-top{position:absolute;display:flex;justify-content:space-between;flex-direction:row;top:-50px}.footer-top .back-top button{font-weight:600;letter-spacing:.2px;color:#191919;font-family:SourceSansProSemiBold,sans-serif;font-size:13px;display:inline;background-color:transparent;border:0;padding:0}.footer-top a.website-feedback-link{align-items:center;color:#191919;display:none}.footer-top .feedback-icon{width:30px;height:30px;margin-right:5px}.footer-main{text-align:center}.footer-logo{margin:40px 0 40px}.footer-george-logo{font-family:LatoRegular,sans-serif}@media (min-width:320px){.footer-george-logo{position:absolute;bottom:16px;width:95%;text-align:center}}.footer-links{font-family:SourceSansProSemiBold,sans-serif;margin:8px 0;display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-ms-justify-content:center;justify-content:center}.footer-links ul{display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;-moz-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;max-width:320px;-webkit-justify-content:center;-ms-justify-content:center;justify-content:center;width:100%}.footer-links li{font-size:.8125em;letter-spacing:.2px}.theme-ghs .footer-links li{color:#fff}.theme-george .footer-links li{color:#191919}.theme-george .footer-links li{border-right:1px solid #cbcbcb}.theme-ghs .footer-links li{padding:4px}.theme-george .footer-links li{padding:0}.theme-ghs .footer-links li{padding-right:0}.theme-george .footer-links li{padding-right:4px}.theme-ghs .footer-links li{margin:0}.theme-george .footer-links li{margin:4px}.footer-links li a{font-size:100%}.theme-ghs .footer-links li a{color:#fff}.theme-george .footer-links li a{color:#191919}.theme-ghs .footer-links li a{font-family:SourceSansProSemiBold,sans-serif}.theme-george .footer-links li a{font-family:LatoRegular,sans-serif}.theme-george .footer-links li a{text-decoration:none}.footer-links li a:hover{text-decoration:underline}.footer-links li:last-child{border-right:0}@media (min-width:768px){footer{min-height:102px}.footer-main{display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-ms-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;width:100%}.footer-logo{margin:0}.footer-links{margin:0;order:-1;-webkit-align-self:flex-end;-moz-align-self:flex-end;-ms-align-self:flex-end;align-self:flex-end}.footer-links ul{max-width:100%}.footer-top .back-top{top:-55px}}@media (min-width:320px){.theme-ghs footer{height:205px}.theme-george footer{height:92px}.theme-ghs .footer-wrapper{height:205px}.theme-george .footer-wrapper{height:92px}.footer-top{top:-50px}}@media (min-width:768px){.theme-ghs footer{height:100px}.theme-george footer{height:60px}.theme-ghs .footer-wrapper{height:100px}.theme-george .footer-wrapper{height:60px}.footer-george-logo{font-family:LatoRegular,sans-serif;position:relative;bottom:0;width:auto;text-align:left}}header{height:92px;width:100%;background-color:#fff;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}.theme-ghs header{box-shadow:0 2px 4px 0 #7f7f7f;margin-bottom:32px}.theme-george header{box-shadow:0 2px 4px 0 #cbcbcb;margin-bottom:32px}header[data-apply-onetrust=true]{margin-top:150px}header .header-container{display:flex;height:92px;justify-content:space-between;align-items:center;background-color:#fff;width:100%;max-width:1024px;margin:0 auto;padding:8px}header .header-container #logo{padding:0}header .header-container #logo img{width:93px}.theme-ghs header .header-container nav{margin-bottom:0}.theme-george header .header-container nav{margin-bottom:8px}header .header-container nav button{width:142px;letter-spacing:normal;text-transform:none}.theme-ghs header .header-container nav button{width:200px;background-image:none}.theme-george header .header-container nav button{width:120px;border:0;background-image:none}header .header-container nav button:hover{box-shadow:none}header .header-container nav .need-help{display:none}@media (min-width:320px){header{margin-bottom:24px;height:48px;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}header[data-apply-onetrust=true]{margin-top:150px}.theme-ghs header{margin-bottom:24px}.theme-george header{margin-bottom:24px}header .header-container{height:48px;padding:13px 16px 10px}header .header-container #logo{margin-bottom:0}header .header-container nav{margin-bottom:0}.theme-ghs header .header-container nav{margin-bottom:2px}.theme-george header .header-container nav{margin-bottom:2px}.theme-ghs header .header-container nav button{font-family:SourceSansProSemiBold,sans-serif;width:144px;margin-right:0;margin-left:8px}.theme-george header .header-container nav button{font-family:LatoRegular,sans-serif;width:88px;padding:16px 0 16px 12px;text-align:right;font-size:12px;margin-right:0;margin-left:8px}header .header-container nav button.basic:hover{background-image:none}}@media (min-width:481px){header{height:92px;margin-bottom:24px;padding-top:0;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}header[data-apply-onetrust=true]{margin-top:125px}header .header-container{height:92px;width:100%;padding:16px}.theme-ghs header .header-container{padding-top:20px}.theme-george header .header-container{padding-top:31px}header .header-container #logo img{width:113px}.theme-george header .header-container nav button{border:0}}@media (min-width:768px){header{transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}header[data-apply-onetrust=true]{margin-top:140px}.theme-ghs header .header-container{padding-top:20px}.theme-george header .header-container{padding-top:31px}.theme-ghs header .header-container nav button{text-align:center}.theme-george header .header-container nav button{text-align:center;padding:0}header .header-container nav .need-help{display:inline-block}.theme-ghs header .header-container nav .need-help{background-image:none;text-transform:none;font-size:18px;width:200px}.theme-george header .header-container nav .need-help{border-right:1px solid #cbcbcb;border-radius:0;background-image:none;text-transform:none;font-size:14px;width:120px}.theme-ghs header .header-container nav .back-shop{background-image:none;width:200px;text-transform:none;font-size:18px}.theme-george header .header-container nav .back-shop{background-image:none;width:120px;text-transform:none;font-size:14px}}@media (min-width:961px){header{padding-top:0;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}.theme-ghs header{margin-bottom:32px}.theme-george header{margin-bottom:32px}header[data-apply-onetrust=true]{margin-top:75px}header .header-container{height:92px}.theme-ghs header .header-container{padding-top:20px}.theme-george header .header-container{padding-top:31px}header .header-container #logo img{width:140px}.theme-ghs header .header-container nav button{font-family:SourceSansProSemiBold,sans-serif}.theme-george header .header-container nav button{width:150px;border:0;border-radius:0;padding:0;height:18px;font-family:LatoRegular,sans-serif;margin:0}}@media (min-width:768px){.layout__section{min-height:640px}}@media (min-width:1024px){.layout__section{display:flex}.layout__main{flex:1 1 auto}}@media (min-width:1280px){.site-width.layout__section{margin:0 auto;max-width:1392px;padding:0 16px}.site-width.layout__main{flex:1 1 auto}}.onetrust-consent-sdk-box[data-apply-ot-banner-popup=true] .onetrust-pc-dark-filter.ot-hide{display:block!important}.onetrust-consent-sdk-box #onetrust-banner-sdk:focus{outline-color:none!important;outline-width:0px!important}.onetrust-consent-sdk-box #onetrust-banner-sdk{background-color:#fff!important;max-width:500px;min-width:500px;width:100%;top:23%;left:50%!important;transform:translate(-50%,-25%);padding:16px 22px 16px 24px;border-radius:2px;z-index:2147483646!important}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-accept-btn-handler{background-color:#0073b1;border-color:#0073b1;color:#fff;border-radius:4px}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-pc-btn-handler{background-color:#fff;border:1px solid #0073b1;color:#0073b1;border-radius:4px}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-group-container button{color:#0073b1;display:inline}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-policy-title,.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-dpd-title{width:100%!important}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-policy-text{width:100%!important;line-height:1.5}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-b-addl-desc{width:100%!important;border-right:0;color:#000}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-dpd-content .ot-dpd-desc{line-height:1.5}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-dpd-container{width:100%!important;padding:0!important}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row{display:flex;flex-direction:column}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-group-container{width:100%}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-group-container p,.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-group-container h3{color:#000}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent{position:relative!important;width:100%;left:auto!important;top:auto;transform:translateY(0%);right:0%}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent #onetrust-button-group{display:flex;justify-content:space-evenly}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent #onetrust-button-group #onetrust-pc-btn-handler,.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent #onetrust-button-group #onetrust-accept-btn-handler{margin-right:0px;max-width:112px;width:100%;margin-top:1em}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-policy{margin:0px}.onetrust-consent-sdk-box #onetrust-pc-sdk{background-color:#fff!important;color:#000;max-width:500px!important;min-width:100%;padding:7px 16px 25px}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-content{overflow-x:hidden}.onetrust-consent-sdk-box #onetrust-pc-sdk *:focus{outline:none!important}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-fltr-cnt,.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-anchor{background-color:#fff!important}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-fltr-cnt{width:auto}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-sel-blk{background-color:#fff}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-pc-header{padding:0}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-title,.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-content,.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-category-title{color:#000!important;width:auto;margin:0;padding:0;padding-bottom:5px}.onetrust-consent-sdk-box #onetrust-pc-sdk #onetrust-group-container button{color:#0073b1}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-desc,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-accordion-layout .ot-cat-header{color:#000!important;line-height:1.5!important;min-height:20px}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-lst-title span{color:#000!important;line-height:1.5}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-name{color:#000!important;font-weight:400!important;line-height:1.5}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-desc{margin-bottom:10px}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-desc .privacy-notice-link{color:#0073b1;text-decoration:none}.onetrust-consent-sdk-box #onetrust-pc-sdk #accept-recommended-btn-handler,.onetrust-consent-sdk-box #onetrust-pc-sdk .save-preference-btn-handler,.onetrust-consent-sdk-box #onetrust-pc-sdk #filter-apply-handler,.onetrust-consent-sdk-box #onetrust-pc-sdk #filter-cancel-handler{background-color:#0073b1!important;border-color:#0073b1!important;color:#fff!important;margin-bottom:10px;border-radius:4px;margin-right:25px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-cat-grp{margin-top:0px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-acc-grpdesc,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-subgrp-desc,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-subgrp h5,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-disc h4,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-dets p,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-dets span,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-label-txt{color:#000!important;font-size:12px!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-dets h4,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-pur h4,.onetrust-consent-sdk-box #onetrust-pc-sdk #clear-filters-handler{color:#000!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-vlst-cntr a,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-link{color:#0073b1!important;padding:0px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-vlst-cntr button{color:#0073b1!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob{background-color:#0073b1;border:1px solid #fff}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob:before{background-color:#fff;border-color:#fff;width:9px;height:9px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-switch{width:28px;height:11px;padding:1px 1px 1px 4px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-switch-nob:before{background-color:#fff;width:9px;height:9px;border-radius:5.5px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-switch-nob{background-color:#767676;border-radius:5.5px;padding-left:1px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-acc-hdr{min-height:auto}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-always-active{color:#000}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-pc-footer-logo a{margin-right:25px}.onetrust-consent-sdk-box #onetrust-pc-sdk #filter-btn-handler{width:30px;height:30px;border-radius:4px;background-color:#538316}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-sel-blk{background-color:#fff!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-chkbox label::before{border:1px solid #538316}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-chkbox input:checked~label::before{background-color:#538316}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-plus-minus{width:15px;height:15px}.onetrust-consent-sdk-box #onetrust-pc-sdk #vendor-search-handler{border-radius:4px}@media (max-width:767px){.onetrust-consent-sdk-box #onetrust-banner-sdk{top:28%;max-width:480px;min-width:325px;height:fit-content}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container{padding:0px}}@media (min-width:768px)and (max-width:1023px){.onetrust-consent-sdk-box #onetrust-banner-sdk{top:23%;max-width:452px;min-width:452px;height:fit-content!important}}@media (min-width:768px){.onetrust-consent-sdk-box #onetrust-pc-sdk{min-width:500px!important}}@media (min-width:1024px){.onetrust-consent-sdk-box #onetrust-banner-sdk{height:fit-content}}button,a.reg-link,.register-nav-link{width:200px;height:40px;border:1px solid #191919;border-radius:4px;text-align:center;user-select:none;margin:0 5px;line-height:14px;font-weight:500;letter-spacing:.5px;cursor:pointer;background:transparent;transition:all 200ms ease}.theme-ghs button,.theme-ghs a.reg-link,.theme-ghs .register-nav-link{font-family:SourceSansProSemiBold,sans-serif;height:40px;font-size:18px;padding:11px}.theme-george button,.theme-george a.reg-link,.theme-george .register-nav-link{font-family:LatoBold,sans-serif;text-transform:uppercase;height:42px;font-size:14px;padding:14px}button a,a.reg-link a,.register-nav-link a{color:inherit;font-size:inherit;font-weight:inherit;font-family:inherit}.theme-george button a,.theme-george a.reg-link a,.theme-george .register-nav-link a{text-decoration:none}button.primary,a.reg-link.primary,.register-nav-link.primary{color:#fff;text-decoration:none}.theme-ghs button.primary,.theme-ghs a.reg-link.primary,.theme-ghs .register-nav-link.primary{background-color:#0073b1;border-color:#0073b1}.theme-george button.primary,.theme-george a.reg-link.primary,.theme-george .register-nav-link.primary{background-image:linear-gradient(to bottom,#424242 35%,black);border-color:#191919}button.primary:hover,a.reg-link.primary:hover,.register-nav-link.primary:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-ghs button.primary:hover,.theme-ghs a.reg-link.primary:hover,.theme-ghs .register-nav-link.primary:hover{background-color:#005a8b}.theme-george button.primary:hover,.theme-george a.reg-link.primary:hover,.theme-george .register-nav-link.primary:hover{background-image:linear-gradient(to bottom,black 35%,#424242)}.theme-ghs button.secondary,.theme-ghs a.reg-link.secondary,.theme-ghs .register-nav-link.secondary{color:#fff;background-color:#68a51c;border-color:#68a51c}.theme-george button.secondary,.theme-george a.reg-link.secondary,.theme-george .register-nav-link.secondary{color:#fff;background-color:#191919;background-image:linear-gradient(to bottom,#424242 35%,black);border-color:#191919;text-transform:uppercase}button.secondary:hover,a.reg-link.secondary:hover,.register-nav-link.secondary:hover{background-color:#538316;box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-george button.secondary:hover,.theme-george a.reg-link.secondary:hover,.theme-george .register-nav-link.secondary:hover{background-image:linear-gradient(to bottom,black 35%,#424242)}.theme-ghs button.secondary:disabled,.theme-ghs a.reg-link.secondary:disabled,.theme-ghs .register-nav-link.secondary:disabled{background-color:#68a51c;opacity:.5;cursor:not-allowed}.theme-george button.secondary:disabled,.theme-george a.reg-link.secondary:disabled,.theme-george .register-nav-link.secondary:disabled{background-color:#191919;background-image:linear-gradient(to bottom,#424242 35%,black);opacity:.5;cursor:not-allowed}.theme-ghs button.secondary.empty,.theme-ghs a.reg-link.secondary.empty,.theme-ghs .register-nav-link.secondary.empty{border-color:#68a51c;color:#68a51c;background-color:#fff}.theme-george button.secondary.empty,.theme-george a.reg-link.secondary.empty,.theme-george .register-nav-link.secondary.empty{border-color:#191919;color:#fff}.theme-ghs button.destructive,.theme-ghs a.reg-link.destructive,.theme-ghs .register-nav-link.destructive{color:#d43030;background-color:#fff;border-color:#d43030}button.destructive:hover,a.reg-link.destructive:hover,.register-nav-link.destructive:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-ghs button.destructive:hover,.theme-ghs a.reg-link.destructive:hover,.theme-ghs .register-nav-link.destructive:hover{border-width:2px}.theme-ghs button.basic,.theme-ghs a.reg-link.basic,.theme-ghs .register-nav-link.basic{background-color:#fff;border-color:#7ebd2f;color:#68a51c}.theme-george button.basic,.theme-george a.reg-link.basic,.theme-george .register-nav-link.basic{background-image:linear-gradient(to bottom,#fdfdfd,#d8d8d8);border-color:#b9b9b9;color:#191919}button.basic:hover,a.reg-link.basic:hover,.register-nav-link.basic:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-ghs button.basic:hover,.theme-ghs a.reg-link.basic:hover,.theme-ghs .register-nav-link.basic:hover{border-width:2px}.theme-george button.basic:hover,.theme-george a.reg-link.basic:hover,.theme-george .register-nav-link.basic:hover{background-image:linear-gradient(to bottom,#d8d8d8,white);border-width:1px}button.full,a.reg-link.full,.register-nav-link.full{width:100%;border-radius:4px;margin:0}@media (min-width:320px){button.half,a.reg-link.half,.register-nav-link.half{width:100%}}@media (min-width:768px){button.half,a.reg-link.half,.register-nav-link.half{width:40%;float:left;margin-right:8px}}button.center,a.reg-link.center,.register-nav-link.center{margin:20px auto;display:block;float:none}@keyframes circle1s{0%{left:0;top:0;width:15px;height:15px;z-index:100}25%{left:26.25px;top:0;width:15px;height:15px;z-index:100}50%{left:52.5px;top:0;width:15px;height:15px;z-index:100}75%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}76%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}100%{left:0;top:0;width:15px;height:15px;z-index:100}}@keyframes circle2s{0%{left:26.25px;top:0;width:15px;height:15px;z-index:100}25%{left:52.5px;top:0;width:15px;height:15px;z-index:100}50%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}51%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}75%{left:0;top:0;width:15px;height:15px;z-index:100}100%{left:26.25px;top:0;width:15px;height:15px;z-index:100}}@keyframes circle3s{0%{left:52.5px;top:0;width:15px;height:15px;z-index:100}25%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}26%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}50%{left:0;top:0;width:15px;height:15px;z-index:100}75%{left:26.25px;top:0;width:15px;height:15px;z-index:100}100%{left:52.5px;top:0;width:15px;height:15px;z-index:100}}@keyframes circle4s{0%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}25%{left:0;top:0;width:15px;height:15px;z-index:100}50%{left:26.25px;top:0;width:15px;height:15px;z-index:100}75%{left:52.5px;top:0;width:15px;height:15px;z-index:100}100%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}}button #loading,a.reg-link #loading,.register-nav-link #loading{display:block;margin:0 auto;width:67.5px;height:15px;position:relative}.theme-ghs button #loading,.theme-ghs a.reg-link #loading,.theme-ghs .register-nav-link #loading{margin-top:0}.theme-george button #loading,.theme-george a.reg-link #loading,.theme-george .register-nav-link #loading{margin-top:-2px}button #loading .animation,a.reg-link #loading .animation,.register-nav-link #loading .animation{position:relative}button #loading .animation .circle,a.reg-link #loading .animation .circle,.register-nav-link #loading .animation .circle{position:absolute;border-radius:100%;border:1px solid #fff;animation-name:circle1s;animation-duration:1.1s;animation-iteration-count:infinite}.theme-ghs button #loading .animation .circle,.theme-ghs a.reg-link #loading .animation .circle,.theme-ghs .register-nav-link #loading .animation .circle{background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle,.theme-george a.reg-link #loading .animation .circle,.theme-george .register-nav-link #loading .animation .circle{background-color:#d9dddf;border-color:#d9dddf}button #loading .animation .circle:nth-child(2),a.reg-link #loading .animation .circle:nth-child(2),.register-nav-link #loading .animation .circle:nth-child(2){animation-name:circle2s}.theme-ghs button #loading .animation .circle:nth-child(2),.theme-ghs a.reg-link #loading .animation .circle:nth-child(2),.theme-ghs .register-nav-link #loading .animation .circle:nth-child(2){background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle:nth-child(2),.theme-george a.reg-link #loading .animation .circle:nth-child(2),.theme-george .register-nav-link #loading .animation .circle:nth-child(2){background-color:#b8babd;border-color:#b8babd}button #loading .animation .circle:nth-child(3),a.reg-link #loading .animation .circle:nth-child(3),.register-nav-link #loading .animation .circle:nth-child(3){animation-name:circle3s}.theme-ghs button #loading .animation .circle:nth-child(3),.theme-ghs a.reg-link #loading .animation .circle:nth-child(3),.theme-ghs .register-nav-link #loading .animation .circle:nth-child(3){background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle:nth-child(3),.theme-george a.reg-link #loading .animation .circle:nth-child(3),.theme-george .register-nav-link #loading .animation .circle:nth-child(3){background-color:#a3a3ab;border-color:#a3a3ab}button #loading .animation .circle:nth-child(4),a.reg-link #loading .animation .circle:nth-child(4),.register-nav-link #loading .animation .circle:nth-child(4){animation-name:circle4s}.theme-ghs button #loading .animation .circle:nth-child(4),.theme-ghs a.reg-link #loading .animation .circle:nth-child(4),.theme-ghs .register-nav-link #loading .animation .circle:nth-child(4){background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle:nth-child(4),.theme-george a.reg-link #loading .animation .circle:nth-child(4),.theme-george .register-nav-link #loading .animation .circle:nth-child(4){background-color:#efefef;border-color:#efefef}a.reg-link,.register-nav-link{display:block;-webkit-font-smoothing:auto}.saved{width:88px;margin:0 auto;display:block}.saved .tick{float:left;margin-right:8px;margin-top:-2px}.saved .saved-text{width:auto;float:left}label.check-box{clear:both;margin-bottom:16px;display:block;font-size:.875em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;padding-left:34px;line-height:1.5;color:#3d3d3d}.theme-ghs label.check-box{font-family:SourceSansProRegular,sans-serif}.theme-george label.check-box{font-family:LatoRegular,sans-serif}label.check-box .checkmark{position:absolute;top:0;left:0;border:1px solid;background-color:#fff}.theme-ghs label.check-box .checkmark{width:20px;height:20px;border-radius:2px;border-color:#68a51c}.theme-george label.check-box .checkmark{width:24px;height:24px;border-radius:5px;border-color:#ccc}label.check-box .checkmark.error{border-color:#d43030}label.check-box .checkmark:after{content:"";position:absolute;display:none;border:solid #fff;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.theme-ghs label.check-box .checkmark:after{left:6px;top:0;width:6px;height:14px;border-width:0 2px 2px 0}.theme-george label.check-box .checkmark:after{left:7px;top:3px;width:8px;height:13px;border-width:0 3px 3px 0}.theme-ghs label.check-box .checkmark.disabled{background-color:#fff;border:1px solid #7ebd2f;opacity:.5;cursor:not-allowed!important}.theme-george label.check-box .checkmark.disabled{background-color:#d8d8d8;border:1px solid #d8d8d8;opacity:.5;cursor:not-allowed!important}label.check-box input[type=checkbox]{position:absolute;opacity:0;height:20px;width:20px;margin:0;left:0}label.check-box input[type=checkbox]:focus~.checkmark,label.check-box input[type=checkbox]:active~.checkmark{outline:#75aee8 auto 5px}.theme-ghs label.check-box input[type=checkbox]:checked~.checkmark{background-color:#68a51c}.theme-george label.check-box input[type=checkbox]:checked~.checkmark{background-color:#191919}label.check-box input[type=checkbox]:checked~.checkmark:after{display:block}label.check-box-tc{padding-left:54px}label.check-box-tc .checkmark{width:40px;height:40px;border-radius:8px;border:solid 2px #767676;background-color:#fff}label.check-box-tc .checkmark:after{left:14px;top:6px;width:12.3px;height:17.8px;border:solid #68a51c;border-width:0 3px 3px 0}label.check-box-tc input[type=checkbox]:checked~.checkmark{background-color:#fff}.input-error{margin-bottom:8px;margin-top:4px}.input-box{position:relative}.input-box__wrapper{position:relative;overflow-wrap:break-word}.theme-ghs .input-box__wrapper.read-only{font-size:1em;font-family:SourceSansProRegular,sans-serif;color:#3d3d3d}.theme-george .input-box__wrapper.read-only{font-size:.875em;font-family:LatoRegular,sans-serif;color:#191919}.input-box.half{width:calc(50% - 4px);margin-right:4px;float:left}.input-box.half.last{margin-left:4px;margin-right:0}.input-box.custom-icon{position:relative}.input-box.custom-icon:before{content:"";position:absolute}.input-box.custom-icon input{padding-left:38px}.input-box label{padding-bottom:4px;display:block}.theme-ghs .input-box label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em;padding:0 0 4px}.theme-george .input-box label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px;padding:0 0 8px}.input-box .alert{position:absolute;top:3px;right:12px}.theme-ghs .input-box .alert{display:block}.theme-george .input-box .alert{display:none}.input-box input{width:100%;border-radius:4px;border:1px solid #ccc;clear:both;transition:border 200ms ease,box-shadow 200ms ease}.theme-ghs .input-box input{height:40px;font-size:1em;padding:12px;margin-bottom:12px;font-family:SourceSansProRegular,sans-serif;color:#3d3d3d}.theme-george .input-box input{height:42px;font-size:.875em;padding:14px 12px;margin-bottom:16px;font-family:LatoRegular,sans-serif;color:#191919}.input-box input:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1);cursor:default}.input-box input:focus{border-color:#767676}.input-box input::placeholder{color:#767676}.input-box input.half{width:40%;float:left;margin-right:10}.input-box input.error{margin-bottom:0}.theme-ghs .input-box input.error{border:2px solid #d43030}.theme-george .input-box input.error{border:1px solid #da291c}.input-box input.read-only{border:0;outline:0;padding:0;margin:0;height:20px;pointer-events:none;background:0;box-shadow:none}.input-box.check-box-grouped label.check-box{width:20px;padding-left:unset;float:left}.input-box.check-box-grouped input[type=checkbox]{width:20px;top:0;left:0;height:20px;padding:0;margin:0}.input-box.check-box-grouped label{line-height:20px;padding-left:34px}.input-box.check-box-grouped .input-error{margin-top:-10px}.theme-ghs .input-error{margin:4px 0 16px}.theme-george .input-error{margin:8px 0 16px}.input-warning{padding:8px;margin:-1px 0 10px 16px;background-color:#ebf1f5;color:#000;font-size:.75em;font-family:SourceSansProRegular,sans-serif;z-index:100;display:inline-block;position:relative}.input-warning .triangle{position:absolute;top:-20px}.input-label{padding-bottom:4px;display:block}.theme-ghs .input-label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em}.theme-george .input-label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px}label.radio{margin-bottom:16px;display:block;font-size:.875em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;padding-left:28px;padding-right:16px;line-height:1.5}.theme-ghs label.radio{font-family:SourceSansProRegular,sans-serif}.theme-george label.radio{font-family:LatoRegular,sans-serif}label.radio .checkmark{position:absolute;top:0;left:0;height:20px;width:20px;background-color:#fff;border-radius:90px;transition:all 250ms ease}.theme-ghs label.radio .checkmark{border:1px solid #68a51c}.theme-george label.radio .checkmark{border:1px solid #191919}label.radio .checkmark:hover{border-width:2px}label.radio .checkmark.error{border-color:#d43030}label.radio.property-type-radio{float:left}label.radio input[type=radio]{position:absolute;opacity:0}label.radio input[type=radio]:focus~.checkmark,label.radio input[type=radio]:active~.checkmark{outline:#75aee8 auto 5px}.theme-ghs label.radio input[type=radio]:checked~.checkmark{border:1px solid #68a51c}.theme-george label.radio input[type=radio]:checked~.checkmark{border:1px solid #191919}label.radio input[type=radio]:checked~.checkmark:before{content:"";display:inline-block;width:18px;height:18px;position:relative;border-radius:100%;vertical-align:top;text-align:center;cursor:pointer;transition:all 250ms ease}.theme-ghs label.radio input[type=radio]:checked~.checkmark:before{background-color:#68a51c}.theme-george label.radio input[type=radio]:checked~.checkmark:before{background-color:#fff}.theme-ghs label.radio input[type=radio]:checked~.checkmark:before{box-shadow:inset 0 0 0 4px #fff}.theme-george label.radio input[type=radio]:checked~.checkmark:before{box-shadow:inset 0 0 0 5px #191919}@keyframes circle1{0%{left:0;top:0;width:40px;height:40px;z-index:100}25%{left:70px;top:0;width:40px;height:40px;z-index:100}50%{left:140px;top:0;width:40px;height:40px;z-index:100}75%{left:140px;top:20px;width:0;height:0;z-index:0}76%{left:20px;top:20px;width:0;height:0;z-index:0}100%{left:0;top:0;width:40px;height:40px;z-index:100}}@keyframes circle2{0%{left:70px;top:0;width:40px;height:40px;z-index:100}25%{left:140px;top:0;width:40px;height:40px;z-index:100}50%{left:140px;top:20px;width:0;height:0;z-index:0}51%{left:20px;top:20px;width:0;height:0;z-index:0}75%{left:0;top:0;width:40px;height:40px;z-index:100}100%{left:70px;top:0;width:40px;height:40px;z-index:100}}@keyframes circle3{0%{left:140px;top:0;width:40px;height:40px;z-index:100}25%{left:140px;top:20px;width:0;height:0;z-index:0}26%{left:20px;top:20px;width:0;height:0;z-index:0}50%{left:0;top:0;width:40px;height:40px;z-index:100}75%{left:70px;top:0;width:40px;height:40px;z-index:100}100%{left:140px;top:0;width:40px;height:40px;z-index:100}}@keyframes circle4{0%{left:20px;top:20px;width:0;height:0;z-index:0}25%{left:0;top:0;width:40px;height:40px;z-index:100}50%{left:70px;top:0;width:40px;height:40px;z-index:100}75%{left:140px;top:0;width:40px;height:40px;z-index:100}100%{left:140px;top:20px;width:0;height:0;z-index:0}}#loading{display:block;margin:calc(50vh - 171px) auto;width:180px;height:40px}#loading .animation{position:relative}#loading .animation .circle{position:absolute;background-color:#d9dddf;border-radius:100%;border:1px solid #d9dddf;animation-name:circle1;animation-duration:1.1s;animation-iteration-count:infinite}#loading .animation .circle:nth-child(2){background-color:#b8babd;border-color:#b8babd;animation-name:circle2}#loading .animation .circle:nth-child(3){background-color:#a3a3ab;border-color:#a3a3ab;animation-name:circle3}#loading .animation .circle:nth-child(4){background-color:#efefef;border-color:#efefef;animation-name:circle4}.asda-backdrop{bottom:0;left:0;position:fixed;right:0;top:0;z-index:10}.asda-backdrop[data-color=transparent]{background-color:transparent}.asda-backdrop[data-color=black]{background-color:rgba(68,70,82,.88)}.dropdown-list-item.selected,.dropdown-list-item:hover{color:#fff;background-color:#767676}.dropdown-wrapper-single{user-select:none;position:relative;width:265px}.dropdown-wrapper-single .dropdown-header{border:1px solid #ccc}.dropdown-wrapper-single .dropdown-header .dropdown-header-name{font-weight:400}.dropdown-wrapper-single .dropdown-list{border:1px solid #ccc;border-top:0}.dropdown-wrapper{font-family:SourceSansProRegular,sans-serif;font-size:24px;font-weight:600;font-style:normal;font-stretch:normal;line-height:normal;letter-spacing:.3px;float:left;margin-right:16px}.dropdown-wrapper .dropdown-header{display:flex;align-items:center;justify-content:space-between;width:176px;height:78px;border:1px solid #767676;cursor:default;position:relative;background-color:#fff;padding:20px}.dropdown-wrapper .dropdown-header.active{border-bottom:0px;top:1px;z-index:11}.dropdown-wrapper .list-wrapper .dropdown-list{z-index:10;position:absolute;width:560px;height:636px;border:1px solid #767676;background-color:#fff;padding:20px 75px 20px 20px;overflow-y:scroll;color:#767676}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{width:100%;padding:8px 0px;line-height:1.54px;cursor:default;display:flex;align-items:center;justify-content:space-between;text-overflow:ellipsis}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:nth-child(5){padding-bottom:18px;border-bottom:2px solid #979797}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px;text-align:left;padding-left:15px}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px;background-color:none}.dropdown-wrapper .dropdown-list::-webkit-scrollbar-thumb{background-image:var(--sf-img-15);height:109px;background-repeat:no-repeat}.dropdown-wrapper .dropdown-list-item.selected,.dropdown-wrapper .dropdown-list-item:hover{color:#fff;background-color:#767676}div[class^=country-flag-]{width:34px;height:20px;background-size:34px 20px}div[class^=country-flag-] img{width:34px;height:20px;background-repeat:no-repeat;background-size:34px 20px}.modal-wrapper{position:absolute;z-index:1000;top:206px;width:802px;height:300px;border-radius:14px;box-shadow:2px 3px 8px 3px rgba(0,0,0,.1);border:solid 1px #ccc;background-color:#fff;padding:30px;font-family:SourceSansProRegular,sans-serif;font-weight:600;font-style:normal;font-stretch:normal;line-height:normal;color:#3d3d3d}.phone-input{width:100%}@media (min-width:320px){.phone-input{display:flex;flex-direction:column}}@media (min-width:481px){.phone-input{flex-direction:row;height:52px}}.phone-input .dropdown-wrapper{float:left;height:42px;margin-right:8px;color:#3d3d3d;font-family:SourceSansProRegular,sans-serif;transition:all 200ms ease;font-size:1em;user-select:none}@media (min-width:320px){.phone-input .dropdown-wrapper{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper{width:32%}}.phone-input .dropdown-wrapper .dropdown-header{cursor:pointer;width:100%;height:40px;padding:0 8px;border-radius:4px;border:1px solid #ccc;transition:box-shadow 200ms ease}.phone-input .dropdown-wrapper .dropdown-header.active{height:41px;border-top-color:#767676;border-right-color:#767676;border-left-color:#767676;border-bottom-left-radius:0;border-bottom-right-radius:0;top:0}.phone-input .dropdown-wrapper .dropdown-header.active .country-code{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active div[class^=country-flag-] img{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active .down-arrow{transform:rotate(0.5turn);padding-bottom:4px}.phone-input .dropdown-wrapper .dropdown-header:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.phone-input .dropdown-wrapper .dropdown-header .country-code{font-size:16px;padding-left:4px}@media (min-width:320px){.phone-input .dropdown-wrapper .dropdown-header .country-code{flex:1;padding-left:15px;display:flex;align-items:center}}.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{width:100px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:215px}@media (min-width:481px){.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{display:none}}.phone-input .dropdown-wrapper .dropdown-header .down-arrow{width:20px;padding-top:4px}.phone-input .dropdown-wrapper .dropdown-header div[class^=country-flag-] img{width:32px;background-repeat:no-repeat;background-size:34px 20px}.phone-input .dropdown-wrapper .list-wrapper{position:relative}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{margin-top:-1px;z-index:10;position:absolute;height:200px;border:1px solid #767676;border-radius:0 0 4px 4px;background-color:#fff;padding:0;overflow-y:scroll;color:#767676}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:372px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list::-webkit-scrollbar *{background:transparent}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{color:#191919;height:40px;font-size:1em}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:hover{background-color:#f6f6f6}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 8px 12px 8px;height:48px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 80px 12px 8px;height:40px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{line-height:initial}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:calc(100% - 32px)}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{text-align:right}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:60px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px}}@media (min-width:320px){.phone-input .input-box{width:100%;margin-top:12px}}@media (min-width:481px){.phone-input .input-box{width:calc(68% - 8px);margin-top:0px}}.divider{width:100%;border-top:1px solid #cbcbcb;margin-bottom:24px}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle{margin-left:-8px;position:absolute}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{box-sizing:content-box;position:absolute;border:8px solid transparent;height:0;width:1px}.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{content:"";z-index:-1;border-width:8px;left:-8px}.theme-ghs .react-datepicker__year-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-year-read-view--down-arrow::before,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle::before,.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle::before{border-bottom-color:#538316}.theme-george .react-datepicker__year-read-view--down-arrow::before,.theme-george .react-datepicker__month-read-view--down-arrow::before,.theme-george .react-datepicker__month-year-read-view--down-arrow::before,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle::before,.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle::before{border-bottom-color:#020202}.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle{top:0;margin-top:-8px}.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{border-top:0}.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle,.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle::before{border-bottom-color:#538316}.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle,.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle::before{border-bottom-color:#020202}.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{top:-1px}.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle::before{border-bottom-color:#538316}.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle::before{border-bottom-color:#020202}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle{bottom:0;margin-bottom:-8px}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before{border-bottom:0}.theme-ghs .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view--down-arrow,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle,.theme-ghs .react-datepicker__year-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-year-read-view--down-arrow::before,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle::before{border-top-color:#538316}.theme-george .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view--down-arrow,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle,.theme-george .react-datepicker__year-read-view--down-arrow::before,.theme-george .react-datepicker__month-read-view--down-arrow::before,.theme-george .react-datepicker__month-year-read-view--down-arrow::before,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle::before{border-top-color:#020202}.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before{bottom:-1px}.theme-ghs .react-datepicker__year-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-year-read-view--down-arrow::before,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle::before{border-top-color:#538316}.theme-george .react-datepicker__year-read-view--down-arrow::before,.theme-george .react-datepicker__month-read-view--down-arrow::before,.theme-george .react-datepicker__month-year-read-view--down-arrow::before,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle::before{border-top-color:#020202}.react-datepicker-wrapper{display:inline-block;padding:0;border:0;width:100%}.react-datepicker_input-default{font-size:13px;border-radius:4px;line-height:16px;padding:6px 10px 5px;width:100%;height:40px}.theme-ghs .react-datepicker_input-default{border:1px solid #538316}.theme-george .react-datepicker_input-default{border:1px solid #020202}.react-datepicker_input-default:hover{box-shadow:0px 2px 10px 0px rgba(0,0,0,.1)}.theme-ghs .react-datepicker_input-default:focus{border-color:#37570f}.theme-george .react-datepicker_input-default:focus{border-color:#000}.react-datepicker{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:.8rem;background-color:#fff;border-radius:.3rem;display:inline-block;position:relative}.theme-ghs .react-datepicker{color:#000;border:1px solid #538316}.theme-george .react-datepicker{color:#000;border:1px solid #020202}.react-datepicker--time-only .react-datepicker__triangle{left:35px}.react-datepicker--time-only .react-datepicker__time-container{border-left:0}.react-datepicker--time-only .react-datepicker__time{border-radius:.3rem}.react-datepicker--time-only .react-datepicker__time-box{border-radius:.3rem}.react-datepicker__triangle{position:absolute;left:50px}.react-datepicker-popper{z-index:1}.react-datepicker-popper[data-placement=bottom-end] .react-datepicker__triangle,.react-datepicker-popper[data-placement=top-end] .react-datepicker__triangle{left:auto;right:50px}.react-datepicker-popper[data-placement^=top]{margin-bottom:10px}.react-datepicker-popper[data-placement^=right]{margin-left:8px}.react-datepicker-popper[data-placement^=right] .react-datepicker__triangle{left:auto;right:42px}.react-datepicker-popper[data-placement^=left]{margin-right:8px}.react-datepicker-popper[data-placement^=left] .react-datepicker__triangle{left:42px;right:auto}.react-datepicker__header{text-align:center;border-top-left-radius:.3rem;border-top-right-radius:.3rem;padding-top:8px;position:relative}.theme-ghs .react-datepicker__header{background-color:#538316;border-bottom:1px solid #538316}.theme-george .react-datepicker__header{background-color:#020202;border-bottom:1px solid #020202}.react-datepicker__header--time{padding-bottom:8px;padding-left:5px;padding-right:5px}.react-datepicker__year-dropdown-container--select,.react-datepicker__month-dropdown-container--select,.react-datepicker__month-year-dropdown-container--select,.react-datepicker__year-dropdown-container--scroll,.react-datepicker__month-dropdown-container--scroll,.react-datepicker__month-year-dropdown-container--scroll{display:inline-block;margin:0 2px}.react-datepicker__current-month,.react-datepicker-time__header,.react-datepicker-year-header{margin-top:0;font-weight:bold;font-size:.944rem;margin-bottom:8px}.theme-ghs .react-datepicker__current-month,.theme-ghs .react-datepicker-time__header,.theme-ghs .react-datepicker-year-header{color:#fff}.theme-george .react-datepicker__current-month,.theme-george .react-datepicker-time__header,.theme-george .react-datepicker-year-header{color:#fff}.react-datepicker-time__header{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.react-datepicker__navigation{background:0;line-height:1.7rem;text-align:center;cursor:pointer;position:absolute;top:10px;width:0;padding:0;border:.45rem solid transparent;z-index:1;height:10px;width:10px;text-indent:-999em;overflow:hidden}.react-datepicker__navigation--previous{left:10px;padding:0!important;height:0!important}.theme-ghs .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-george .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-ghs .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.theme-george .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.react-datepicker__navigation--previous--disabled,.react-datepicker__navigation--previous--disabled:hover{cursor:default}.theme-ghs .react-datepicker__navigation--previous--disabled,.theme-ghs .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.theme-george .react-datepicker__navigation--previous--disabled,.theme-george .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.react-datepicker__navigation--next{right:10px;padding:0!important;height:0!important}.theme-ghs .react-datepicker__navigation--next{border-left-color:#ccc}.theme-george .react-datepicker__navigation--next{border-left-color:#ccc}.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button){right:80px}.theme-ghs .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.theme-george .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.react-datepicker__navigation--next--disabled,.react-datepicker__navigation--next--disabled:hover{cursor:default}.theme-ghs .react-datepicker__navigation--next--disabled,.theme-ghs .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.theme-george .react-datepicker__navigation--next--disabled,.theme-george .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.react-datepicker__navigation--years{position:relative;top:0;display:block;margin-left:auto;margin-right:auto}.react-datepicker__navigation--years-previous{top:4px}.theme-ghs .react-datepicker__navigation--years-previous{border-top-color:#ccc}.theme-george .react-datepicker__navigation--years-previous{border-top-color:#ccc}.theme-ghs .react-datepicker__navigation--years-previous:hover{border-top-color:#b3b3b3}.theme-george .react-datepicker__navigation--years-previous:hover{border-top-color:#b3b3b3}.react-datepicker__navigation--years-upcoming{top:-4px}.theme-ghs .react-datepicker__navigation--years-upcoming{border-bottom-color:#ccc}.theme-george .react-datepicker__navigation--years-upcoming{border-bottom-color:#ccc}.theme-ghs .react-datepicker__navigation--years-upcoming:hover{border-bottom-color:#b3b3b3}.theme-george .react-datepicker__navigation--years-upcoming:hover{border-bottom-color:#b3b3b3}.react-datepicker__month-container{float:left}.react-datepicker__month{margin:.4rem;text-align:center}.react-datepicker__month .react-datepicker__month-text,.react-datepicker__month .react-datepicker__quarter-text{display:inline-block;width:4rem;margin:2px}.react-datepicker__input-time-container{clear:both;width:100%;float:left;margin:5px 0 10px 15px;text-align:left}.react-datepicker__input-time-container .react-datepicker-time__caption{display:inline-block}.react-datepicker__input-time-container .react-datepicker-time__input-container{display:inline-block}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input{display:inline-block;margin-left:10px}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input{width:85px}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input[type=time]::-webkit-inner-spin-button,.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input[type=time]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input[type=time]{-moz-appearance:textfield}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__delimiter{margin-left:5px;display:inline-block}.react-datepicker__time-container{float:right;width:85px}.theme-ghs .react-datepicker__time-container{border-left:1px solid #538316}.theme-george .react-datepicker__time-container{border-left:1px solid #020202}.react-datepicker__time-container--with-today-button{display:inline;border:1px solid #aeaeae;border-radius:.3rem;position:absolute;right:-72px;top:0}.react-datepicker__time-container .react-datepicker__time{position:relative;background:#fff}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box{width:85px;overflow-x:hidden;margin:0 auto;text-align:center}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list{list-style:none;margin:0;height:calc(195px + (1.7rem / 2));overflow-y:scroll;padding-right:0px;padding-left:0px;width:100%;box-sizing:content-box}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item{height:30px;padding:5px 10px;white-space:nowrap}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{cursor:pointer}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{background-color:#538316}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{background-color:#020202}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected{color:#fff;font-weight:bold}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected{background-color:#538316}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected:hover{background-color:#538316}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected{background-color:#020202}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected:hover{background-color:#020202}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled{color:#ccc}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled{color:#ccc}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled:hover{cursor:default;background-color:transparent}.react-datepicker__week-number{display:inline-block;width:1.7rem;line-height:1.7rem;text-align:center;margin:.166rem}.theme-ghs .react-datepicker__week-number{color:#ccc}.theme-george .react-datepicker__week-number{color:#ccc}.react-datepicker__week-number.react-datepicker__week-number--clickable{cursor:pointer}.react-datepicker__week-number.react-datepicker__week-number--clickable:hover{border-radius:.3rem}.theme-ghs .react-datepicker__week-number.react-datepicker__week-number--clickable:hover{background-color:#538316}.theme-george .react-datepicker__week-number.react-datepicker__week-number--clickable:hover{background-color:#020202}.react-datepicker__day-names,.react-datepicker__week{white-space:nowrap}.react-datepicker__day,.react-datepicker__day-name,.react-datepicker__time-name{display:inline-block;width:1.7rem;line-height:1.7rem;text-align:center;margin:.166rem}.theme-ghs .react-datepicker__day,.theme-ghs .react-datepicker__day-name,.theme-ghs .react-datepicker__time-name{color:#000}.theme-george .react-datepicker__day,.theme-george .react-datepicker__day-name,.theme-george .react-datepicker__time-name{color:#000}.theme-ghs .react-datepicker__day-name{color:#fff}.theme-george .react-datepicker__day-name{color:#fff}.react-datepicker__month--selected,.react-datepicker__month--in-selecting-range,.react-datepicker__month--in-range,.react-datepicker__quarter--selected,.react-datepicker__quarter--in-selecting-range,.react-datepicker__quarter--in-range{border-radius:.3rem;color:#fff}.theme-ghs .react-datepicker__month--selected,.theme-ghs .react-datepicker__month--in-selecting-range,.theme-ghs .react-datepicker__month--in-range,.theme-ghs .react-datepicker__quarter--selected,.theme-ghs .react-datepicker__quarter--in-selecting-range,.theme-ghs .react-datepicker__quarter--in-range{background-color:#538316}.theme-ghs .react-datepicker__month--selected:hover,.theme-ghs .react-datepicker__month--in-selecting-range:hover,.theme-ghs .react-datepicker__month--in-range:hover,.theme-ghs .react-datepicker__quarter--selected:hover,.theme-ghs .react-datepicker__quarter--in-selecting-range:hover,.theme-ghs .react-datepicker__quarter--in-range:hover{background-color:#456d12}.theme-george .react-datepicker__month--selected,.theme-george .react-datepicker__month--in-selecting-range,.theme-george .react-datepicker__month--in-range,.theme-george .react-datepicker__quarter--selected,.theme-george .react-datepicker__quarter--in-selecting-range,.theme-george .react-datepicker__quarter--in-range{background-color:#020202}.theme-george .react-datepicker__month--selected:hover,.theme-george .react-datepicker__month--in-selecting-range:hover,.theme-george .react-datepicker__month--in-range:hover,.theme-george .react-datepicker__quarter--selected:hover,.theme-george .react-datepicker__quarter--in-selecting-range:hover,.theme-george .react-datepicker__quarter--in-range:hover{background-color:#000}.react-datepicker__month--disabled,.react-datepicker__quarter--disabled{pointer-events:none}.theme-ghs .react-datepicker__month--disabled,.theme-ghs .react-datepicker__quarter--disabled{color:#ccc}.theme-george .react-datepicker__month--disabled,.theme-george .react-datepicker__quarter--disabled{color:#ccc}.react-datepicker__month--disabled:hover,.react-datepicker__quarter--disabled:hover{cursor:default;background-color:transparent}.react-datepicker__day,.react-datepicker__day-name,.react-datepicker__month-text,.react-datepicker__quarter-text{cursor:pointer}.react-datepicker__day:hover,.react-datepicker__day-name:hover,.react-datepicker__month-text:hover,.react-datepicker__quarter-text:hover{border-radius:.3rem}.theme-ghs .react-datepicker__day:hover,.theme-ghs .react-datepicker__day-name:hover,.theme-ghs .react-datepicker__month-text:hover,.theme-ghs .react-datepicker__quarter-text:hover{background-color:#68a51c;color:#fff}.theme-george .react-datepicker__day:hover,.theme-george .react-datepicker__day-name:hover,.theme-george .react-datepicker__month-text:hover,.theme-george .react-datepicker__quarter-text:hover{background-color:#3d3d3d;color:#fff}.react-datepicker__day--today,.react-datepicker__month-text--today,.react-datepicker__quarter-text--today{font-weight:bold}.react-datepicker__day--highlighted,.react-datepicker__month-text--highlighted,.react-datepicker__quarter-text--highlighted{border-radius:.3rem;color:#fff}.theme-ghs .react-datepicker__day--highlighted,.theme-ghs .react-datepicker__month-text--highlighted,.theme-ghs .react-datepicker__quarter-text--highlighted{background-color:#68a51c}.theme-ghs .react-datepicker__day--highlighted:hover,.theme-ghs .react-datepicker__month-text--highlighted:hover,.theme-ghs .react-datepicker__quarter-text--highlighted:hover{background-color:#5a8f18}.theme-george .react-datepicker__day--highlighted,.theme-george .react-datepicker__month-text--highlighted,.theme-george .react-datepicker__quarter-text--highlighted{background-color:#3d3d3d}.theme-george .react-datepicker__day--highlighted:hover,.theme-george .react-datepicker__month-text--highlighted:hover,.theme-george .react-datepicker__quarter-text--highlighted:hover{background-color:#303030}.react-datepicker__day--highlighted-custom-1,.react-datepicker__month-text--highlighted-custom-1,.react-datepicker__quarter-text--highlighted-custom-1{color:#f0f}.react-datepicker__day--highlighted-custom-2,.react-datepicker__month-text--highlighted-custom-2,.react-datepicker__quarter-text--highlighted-custom-2{color:green}.theme-ghs .react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-ghs .react-datepicker__month-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-ghs .react-datepicker__quarter-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range){background-color:rgba(83,131,22,.5)}.theme-george .react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-george .react-datepicker__month-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-george .react-datepicker__quarter-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range){background-color:rgba(2,2,2,.5)}.theme-ghs .react-datepicker__month--selecting-range .react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-ghs .react-datepicker__month--selecting-range .react-datepicker__month-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-ghs .react-datepicker__month--selecting-range .react-datepicker__quarter-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range){background-color:#538316;color:#000}.theme-george .react-datepicker__month--selecting-range .react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-george .react-datepicker__month--selecting-range .react-datepicker__month-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-george .react-datepicker__month--selecting-range .react-datepicker__quarter-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range){background-color:#020202;color:#000}.react-datepicker__day--selected,.react-datepicker__day--in-selecting-range,.react-datepicker__day--in-range,.react-datepicker__month-text--selected,.react-datepicker__month-text--in-selecting-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--selected,.react-datepicker__quarter-text--in-selecting-range,.react-datepicker__quarter-text--in-range{border-radius:.3rem}.theme-ghs .react-datepicker__day--selected,.theme-ghs .react-datepicker__day--in-selecting-range,.theme-ghs .react-datepicker__day--in-range,.theme-ghs .react-datepicker__month-text--selected,.theme-ghs .react-datepicker__month-text--in-selecting-range,.theme-ghs .react-datepicker__month-text--in-range,.theme-ghs .react-datepicker__quarter-text--selected,.theme-ghs .react-datepicker__quarter-text--in-selecting-range,.theme-ghs .react-datepicker__quarter-text--in-range{color:#fff;background-color:#538316}.theme-ghs .react-datepicker__day--selected:hover,.theme-ghs .react-datepicker__day--in-selecting-range:hover,.theme-ghs .react-datepicker__day--in-range:hover,.theme-ghs .react-datepicker__month-text--selected:hover,.theme-ghs .react-datepicker__month-text--in-selecting-range:hover,.theme-ghs .react-datepicker__month-text--in-range:hover,.theme-ghs .react-datepicker__quarter-text--selected:hover,.theme-ghs .react-datepicker__quarter-text--in-selecting-range:hover,.theme-ghs .react-datepicker__quarter-text--in-range:hover{background-color:#456d12}.theme-george .react-datepicker__day--selected,.theme-george .react-datepicker__day--in-selecting-range,.theme-george .react-datepicker__day--in-range,.theme-george .react-datepicker__month-text--selected,.theme-george .react-datepicker__month-text--in-selecting-range,.theme-george .react-datepicker__month-text--in-range,.theme-george .react-datepicker__quarter-text--selected,.theme-george .react-datepicker__quarter-text--in-selecting-range,.theme-george .react-datepicker__quarter-text--in-range{color:#fff;background-color:#020202}.theme-george .react-datepicker__day--selected:hover,.theme-george .react-datepicker__day--in-selecting-range:hover,.theme-george .react-datepicker__day--in-range:hover,.theme-george .react-datepicker__month-text--selected:hover,.theme-george .react-datepicker__month-text--in-selecting-range:hover,.theme-george .react-datepicker__month-text--in-range:hover,.theme-george .react-datepicker__quarter-text--selected:hover,.theme-george .react-datepicker__quarter-text--in-selecting-range:hover,.theme-george .react-datepicker__quarter-text--in-range:hover{background-color:#000}.react-datepicker__day--keyboard-selected,.react-datepicker__month-text--keyboard-selected,.react-datepicker__quarter-text--keyboard-selected{border-radius:.3rem}.theme-ghs .react-datepicker__day--keyboard-selected,.theme-ghs .react-datepicker__month-text--keyboard-selected,.theme-ghs .react-datepicker__quarter-text--keyboard-selected{color:#fff;background-color:#6faf1d}.theme-ghs .react-datepicker__day--keyboard-selected:hover,.theme-ghs .react-datepicker__month-text--keyboard-selected:hover,.theme-ghs .react-datepicker__quarter-text--keyboard-selected:hover{background-color:#456d12}.theme-george .react-datepicker__day--keyboard-selected,.theme-george .react-datepicker__month-text--keyboard-selected,.theme-george .react-datepicker__quarter-text--keyboard-selected{color:#fff;background-color:#1c1c1c}.theme-george .react-datepicker__day--keyboard-selected:hover,.theme-george .react-datepicker__month-text--keyboard-selected:hover,.theme-george .react-datepicker__quarter-text--keyboard-selected:hover{background-color:#000}.react-datepicker__day--disabled,.react-datepicker__month-text--disabled,.react-datepicker__quarter-text--disabled{cursor:default}.theme-ghs .react-datepicker__day--disabled,.theme-ghs .react-datepicker__month-text--disabled,.theme-ghs .react-datepicker__quarter-text--disabled{color:#ccc}.theme-george .react-datepicker__day--disabled,.theme-george .react-datepicker__month-text--disabled,.theme-george .react-datepicker__quarter-text--disabled{color:#ccc}.react-datepicker__day--disabled:hover,.react-datepicker__month-text--disabled:hover,.react-datepicker__quarter-text--disabled:hover{background-color:transparent}.theme-ghs .react-datepicker__month-text.react-datepicker__month--selected:hover,.theme-ghs .react-datepicker__month-text.react-datepicker__month--in-range:hover,.theme-ghs .react-datepicker__month-text.react-datepicker__quarter--selected:hover,.theme-ghs .react-datepicker__month-text.react-datepicker__quarter--in-range:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__month--selected:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__month--in-range:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__quarter--selected:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__quarter--in-range:hover{background-color:#538316}.theme-george .react-datepicker__month-text.react-datepicker__month--selected:hover,.theme-george .react-datepicker__month-text.react-datepicker__month--in-range:hover,.theme-george .react-datepicker__month-text.react-datepicker__quarter--selected:hover,.theme-george .react-datepicker__month-text.react-datepicker__quarter--in-range:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__month--selected:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__month--in-range:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__quarter--selected:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__quarter--in-range:hover{background-color:#020202}.theme-ghs .react-datepicker__month-text:hover,.theme-ghs .react-datepicker__quarter-text:hover{background-color:#538316}.theme-george .react-datepicker__month-text:hover,.theme-george .react-datepicker__quarter-text:hover{background-color:#020202}.react-datepicker__input-container{position:relative;display:inline-block;width:100%}.react-datepicker__year-read-view,.react-datepicker__month-read-view,.react-datepicker__month-year-read-view{border:1px solid transparent;border-radius:.3rem}.react-datepicker__year-read-view:hover,.react-datepicker__month-read-view:hover,.react-datepicker__month-year-read-view:hover{cursor:pointer}.theme-ghs .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__year-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view:hover .react-datepicker__month-read-view--down-arrow{border-top-color:#b3b3b3}.theme-george .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__year-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view:hover .react-datepicker__month-read-view--down-arrow{border-top-color:#b3b3b3}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow{float:right;margin-left:20px;top:8px;position:relative;border-width:.45rem}.theme-ghs .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view--down-arrow{border-top-color:#ccc}.theme-george .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view--down-arrow{border-top-color:#ccc}.react-datepicker__year-dropdown,.react-datepicker__month-dropdown,.react-datepicker__month-year-dropdown{position:absolute;width:50%;left:25%;top:30px;z-index:1;text-align:center;border-radius:.3rem}.theme-ghs .react-datepicker__year-dropdown,.theme-ghs .react-datepicker__month-dropdown,.theme-ghs .react-datepicker__month-year-dropdown{background-color:#538316;border:1px solid #538316}.theme-george .react-datepicker__year-dropdown,.theme-george .react-datepicker__month-dropdown,.theme-george .react-datepicker__month-year-dropdown{background-color:#020202;border:1px solid #020202}.react-datepicker__year-dropdown:hover,.react-datepicker__month-dropdown:hover,.react-datepicker__month-year-dropdown:hover{cursor:pointer}.react-datepicker__year-dropdown--scrollable,.react-datepicker__month-dropdown--scrollable,.react-datepicker__month-year-dropdown--scrollable{height:150px;overflow-y:scroll}.react-datepicker__year-option,.react-datepicker__month-option,.react-datepicker__month-year-option{line-height:20px;width:100%;display:block;margin-left:auto;margin-right:auto}.react-datepicker__year-option:first-of-type,.react-datepicker__month-option:first-of-type,.react-datepicker__month-year-option:first-of-type{border-top-left-radius:.3rem;border-top-right-radius:.3rem}.react-datepicker__year-option:last-of-type,.react-datepicker__month-option:last-of-type,.react-datepicker__month-year-option:last-of-type{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-bottom-left-radius:.3rem;border-bottom-right-radius:.3rem}.theme-ghs .react-datepicker__year-option:hover,.theme-ghs .react-datepicker__month-option:hover,.theme-ghs .react-datepicker__month-year-option:hover{background-color:#ccc}.theme-ghs .react-datepicker__year-option:hover .react-datepicker__navigation--years-upcoming,.theme-ghs .react-datepicker__month-option:hover .react-datepicker__navigation--years-upcoming,.theme-ghs .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-upcoming{border-bottom-color:#b3b3b3}.theme-ghs .react-datepicker__year-option:hover .react-datepicker__navigation--years-previous,.theme-ghs .react-datepicker__month-option:hover .react-datepicker__navigation--years-previous,.theme-ghs .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-previous{border-top-color:#b3b3b3}.theme-george .react-datepicker__year-option:hover,.theme-george .react-datepicker__month-option:hover,.theme-george .react-datepicker__month-year-option:hover{background-color:#ccc}.theme-george .react-datepicker__year-option:hover .react-datepicker__navigation--years-upcoming,.theme-george .react-datepicker__month-option:hover .react-datepicker__navigation--years-upcoming,.theme-george .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-upcoming{border-bottom-color:#b3b3b3}.theme-george .react-datepicker__year-option:hover .react-datepicker__navigation--years-previous,.theme-george .react-datepicker__month-option:hover .react-datepicker__navigation--years-previous,.theme-george .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-previous{border-top-color:#b3b3b3}.react-datepicker__year-option--selected,.react-datepicker__month-option--selected,.react-datepicker__month-year-option--selected{position:absolute;left:15px}.react-datepicker__close-icon{cursor:pointer;background-color:transparent;border:0;outline:0;padding:0px 6px 0px 0px;position:absolute;top:0;right:0;height:100%;display:table-cell;vertical-align:middle}.react-datepicker__close-icon::after{cursor:pointer;color:#fff;border-radius:50%;height:16px;width:16px;padding:2px;font-size:12px;line-height:1;text-align:center;display:table-cell;vertical-align:middle;content:"\D7"}.theme-ghs .react-datepicker__close-icon::after{background-color:#538316}.theme-george .react-datepicker__close-icon::after{background-color:#020202}.react-datepicker__today-button{cursor:pointer;text-align:center;font-weight:bold;padding:5px 0;clear:left}.theme-ghs .react-datepicker__today-button{background:#538316;border-top:1px solid #538316}.theme-george .react-datepicker__today-button{background:#020202;border-top:1px solid #020202}.react-datepicker__portal{position:fixed;width:100vw;height:100vh;background-color:rgba(0,0,0,.8);left:0;top:0;justify-content:center;align-items:center;display:flex;z-index:2147483647}.react-datepicker__portal .react-datepicker__day-name,.react-datepicker__portal .react-datepicker__day,.react-datepicker__portal .react-datepicker__time-name{width:3rem;line-height:3rem}@media (max-width:400px),(max-height:550px){.react-datepicker__portal .react-datepicker__day-name,.react-datepicker__portal .react-datepicker__day,.react-datepicker__portal .react-datepicker__time-name{width:2rem;line-height:2rem}}.react-datepicker__portal .react-datepicker__current-month,.react-datepicker__portal .react-datepicker-time__header{font-size:1.44rem}.react-datepicker__portal .react-datepicker__navigation{border:.81rem solid transparent}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.theme-george .react-datepicker__portal .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-george .react-datepicker__portal .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.react-datepicker__portal .react-datepicker__navigation--previous--disabled,.react-datepicker__portal .react-datepicker__navigation--previous--disabled:hover{cursor:default}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous--disabled,.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.theme-george .react-datepicker__portal .react-datepicker__navigation--previous--disabled,.theme-george .react-datepicker__portal .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next{border-left-color:#ccc}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.theme-george .react-datepicker__portal .react-datepicker__navigation--next{border-left-color:#ccc}.theme-george .react-datepicker__portal .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.react-datepicker__portal .react-datepicker__navigation--next--disabled,.react-datepicker__portal .react-datepicker__navigation--next--disabled:hover{cursor:default}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next--disabled,.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.theme-george .react-datepicker__portal .react-datepicker__navigation--next--disabled,.theme-george .react-datepicker__portal .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.datepicker-wrapper{position:relative}.datepicker-wrapper--error input{border:2px solid #d43030}.theme-ghs .what-can-i-expect,.theme-george .what-can-i-expect{font-family:SourceSansProRegular,sans-serif;padding:2px 0 28px 0}.theme-ghs .what-can-i-expect__button,.theme-george .what-can-i-expect__button{font-size:14px;padding:0px;text-transform:inherit}.theme-ghs .what-can-i-expect__chevron,.theme-george .what-can-i-expect__chevron{padding-left:6px}.theme-ghs .what-can-i-expect__chevron--flipped,.theme-george .what-can-i-expect__chevron--flipped{transform:rotate(180deg)}.theme-ghs .what-can-i-expect__description,.theme-george .what-can-i-expect__description{color:#3d3d3d;font-size:14px;line-height:1.5;letter-spacing:.2px;list-style:disc;padding-left:20px;padding-top:14px}.theme-ghs .what-can-i-expect__description li,.theme-george .what-can-i-expect__description li{padding-bottom:16px}.theme-ghs .what-can-i-expect__description li:last-child,.theme-george .what-can-i-expect__description li:last-child{padding-bottom:0px}.theme-george .what-can-i-expect__button{text-decoration:underline}</style><style>html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}article *,article *:before,article *:after{box-sizing:border-box}@font-face{font-family:"SourceSansProBold";src:url(resources/sample/7.woff2) format("woff2")}@font-face{font-family:"SourceSansProSemiBold";src:url(resources/sample/8.woff2) format("woff2")}@font-face{font-family:"SourceSansProRegular";src:url(resources/sample/9.woff2) format("woff2")}h1,h2,h3,h4,p,a{color:#191919;padding-bottom:8px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1{font-size:1.25em;font-weight:700;color:#191919;padding-bottom:16px;line-height:1.25}.theme-ghs h1{font-family:SourceSansProBold,sans-serif}.theme-george h1{font-family:LatoBold,sans-serif}h2{font-size:1.125em;font-weight:800}.theme-ghs h2{font-family:SourceSansProBold,sans-serif}.theme-george h2{font-family:LatoBold,sans-serif}h3{font-size:1em;font-weight:600}.theme-ghs h3{font-family:SourceSansProSemiBold,sans-serif}.theme-george h3{font-family:LatoRegular,sans-serif}h4{font-size:.875em;font-weight:400}.theme-ghs h4{font-family:SourceSansProRegular,sans-serif}.theme-george h4{font-family:LatoRegular,sans-serif}p{color:off-black;padding-bottom:16px;font-size:.875em;letter-spacing:.2px;line-height:1.3}.theme-ghs p{font-family:SourceSansProRegular,sans-serif}.theme-george p{font-family:LatoRegular,sans-serif}a{margin-bottom:16px;padding-bottom:0;font-weight:500;font-size:1em}.theme-ghs a{color:#0073b1;text-decoration:none;font-family:SourceSansProBold,sans-serif}.theme-george a{color:#191919;text-decoration:underline;font-family:LatoRegular,sans-serif}.button-as-link{display:inline;background-color:transparent;border:0;padding:0;width:auto!important;height:auto!important;color:#0073b1;margin-left:0;padding-bottom:16px;font-weight:500;text-decoration:none}.theme-ghs .button-as-link{font-size:.875em}.theme-george .button-as-link{font-size:12px}.theme-ghs .button-as-link{color:#0073b1}.theme-george .button-as-link{color:#191919}.short-password-error{bottom:62px}.blank-password-error{bottom:50px}a:hover{cursor:pointer}.theme-ghs strong{font-family:SourceSansProBold,sans-serif}.theme-george strong{font-family:LatoBold,sans-serif}.input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;line-height:1.125em}.theme-ghs .input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;clear:both}.theme-george .input-error{font-family:LatoBold,sans-serif;font-size:.875em;color:#da291c;clear:both}.theme-ghs .input-error a{font-family:SourceSansProRegular,sans-serif;color:#d43030;text-decoration:underline}.theme-george .input-error a{font-family:LatoBold,sans-serif;color:#da291c;text-decoration:underline}.left{float:left!important}.right{float:right!important;text-align:right!important}.theme-ghs .right{padding-top:3px;line-height:1.5}.theme-george .right{line-height:1.5}.center{text-align:center!important}.red{color:#da0500!important}.orange{color:#fdb45b!important}.pink{color:#ec938e!important}.yellow{color:#fae100!important}.green{color:#68a51c!important}.blue{color:#0073b1!important}.grey{color:grey1!important}.white{color:#fff!important}.black{color:#191919!important}.regular{font-weight:400!important}.bold{font-weight:700!important}html{overflow-x:hidden;width:100vw}.co-account{min-height:calc(100vh - 294px);height:auto;width:calc(100% - 32px);min-height:calc(100vh - 356px);margin:0 16px;position:relative}.theme-ghs .co-account{min-height:calc(100vh - 300px)}.theme-george .co-account{min-height:calc(100vh - 261px)}@media (min-width:481px){.co-account{width:416px;margin:0 auto}.theme-ghs .co-account{min-height:calc(100vh - 284px)}.theme-george .co-account{min-height:calc(100vh - 245px)}}.co-account{height:auto;margin:0 auto 60px}@media (min-width:481px){.co-account{width:406px}}.co-sign-in-details__password-container,.co-sign-in-details__email-container,.co-sign-in-details__phone-container{position:relative;margin-bottom:20px;height:auto;display:block}.co-sign-in-details__password-container{margin-bottom:0}.co-sign-in-details__email-container--read-only .input-box{max-width:86%}.with-whats-this{height:32px}.with-whats-this .whats-this-label{float:left}.address-phone-book .address-section,.address-phone-book .contact-section{margin-bottom:16px}@media (min-width:320px){.address-phone-book .address-section label[for^=account-postcode],.address-phone-book .contact-section label[for^=account-postcode]{height:32px;display:flex;flex-direction:column-reverse}}@media (min-width:481px){.address-phone-book .address-section label[for^=account-postcode],.address-phone-book .contact-section label[for^=account-postcode]{height:auto}}.address-phone-book .address-section .list .list-item,.address-phone-book .contact-section .list .list-item{display:flex}.address-phone-book .address-section .list .list-item .list-item-delete-button img,.address-phone-book .contact-section .list .list-item .list-item-delete-button img{margin-top:0}.permission-centre button.add_phone{margin-bottom:16px}.wallet-card .co-list-card{display:flex;flex-direction:column}.wallet-card .co-list-card .list-item .list-item-radio{padding-right:0}.wallet-card .co-edit-card{margin-top:16px;padding-bottom:16px;border-bottom:1px solid #ccc}.wallet-card .modal{font-weight:400}.wallet-card .co-add-card{padding-top:10px}.wallet-card .co-add-card .custom-icon input{padding-left:50px}.wallet-card .co-add-card .custom-icon:before{left:10px;top:30px;width:30px;height:20px;background-image:var(--sf-img-16);z-index:1}.wallet-card .co-add-card .custom-icon.amex:before{background-image:var(--sf-img-17)}.wallet-card .co-add-card .custom-icon.maestro:before{background-image:var(--sf-img-18)}.wallet-card .co-add-card .custom-icon.mastercard:before{background-image:var(--sf-img-19)}.wallet-card .co-add-card .custom-icon.visa:before{background-image:var(--sf-img-20)}.wallet-card .co-add-card .date-input:before{top:30px;left:10px}.theme-ghs .wallet-card .co-add-card .date-input:before{top:30px}.theme-george .wallet-card .co-add-card .date-input:before{top:35px}@media (min-width:320px){.wallet-card .co-add-card label[for^=account-postcode]{height:32px;display:flex;flex-direction:column-reverse}}@media (min-width:481px){.wallet-card .co-add-card label[for^=account-postcode]{height:auto}}.wallet-card .date-input{position:relative}.wallet-card .date-input input{letter-spacing:.8px}.wallet-card .date-input:before{content:" / ";pointer-events:none;white-space:pre;position:absolute;top:28px;left:13px;display:block;width:40px;height:19px;color:#191919;font-size:1.25em;z-index:1}.co-personal-details__colleague-details{position:relative;margin-bottom:20px;height:auto;display:block}.underlined-link-toggle{border:0;padding:0!important;text-align:left;font-size:14px;width:auto;height:auto;margin:0;text-decoration:underline}.theme-ghs .underlined-link-toggle{color:#0073b1;letter-spacing:normal}.theme-george .underlined-link-toggle{color:#191919;letter-spacing:normal}.co-account .leave-btc .destructive{font-size:19px;margin-top:36px}.co-account .leave-btc .close-icon{position:absolute;top:8px;right:8px;width:auto;height:auto;border:0;padding:0;margin:0;background:transparent}.co-account .leave-btc .modal{width:400px;top:25%}.co-account .leave-btc .modal .modal-title,.co-account .leave-btc .modal .modal-content{border-bottom:0;padding-bottom:0px}.co-account .leave-btc .modal .modal-footer{flex-direction:column-reverse;gap:16px}.co-account .leave-btc .modal .modal-footer button{font-size:19px;font-weight:bold}.co-account .leave-btc .modal .modal-footer button.destructive{margin-top:0px}@media (min-width:481px){.leave-btc .modal{left:calc(50% - 200px)}}.panel{position:relative;border:1px solid #ccc}.theme-ghs .panel{border-radius:4px;margin:0 0 8px}.theme-george .panel{border-radius:0;margin:0 0 16px}.theme-ghs .panel.open{border-color:#ccc;box-shadow:none}.theme-george .panel.open{border-color:#9b9b9b;box-shadow:4px 4px 4px 0 rgba(0,0,0,.08)}.panel__header{width:100%;border:1px solid transparent;text-align:right;line-height:40px;margin:0;overflow:hidden;text-transform:none;letter-spacing:normal}.theme-ghs .panel__header{box-shadow:none;background:#f6f6f6;height:52px;padding:6px 16px;border-radius:4px}.theme-george .panel__header{box-shadow:4px 4px 4px 0 rgba(0,0,0,.08);background:#fff;height:52px;padding:6px 16px;border-radius:0}.panel__header:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.panel__header.add-bottom-border{border:1px solid transparent;border-bottom:1px solid transparent}.theme-ghs .panel__header.add-bottom-border{border-bottom:1px solid #ccc;box-shadow:none}.theme-george .panel__header.add-bottom-border{border-bottom:1px solid #9b9b9b;box-shadow:none}.panel__chevron{padding:16px 0}.panel__toggle{color:#0979b2}.theme-ghs .panel__toggle{font-family:SourceSansProRegular,sans-serif;font-size:18px;font-weight:600}.theme-george .panel__toggle{font-family:LatoRegular,sans-serif;font-size:14px;font-weight:500}.panel__title{float:left;text-transform:none}.theme-ghs .panel__title{font-family:SourceSansProRegular,sans-serif;font-size:18px;font-weight:600}.theme-george .panel__title{font-family:LatoRegular,sans-serif;font-size:14px;font-weight:normal}.panel__section-title{font-size:16px;text-align:left;color:#000;padding-bottom:16px}.theme-ghs .panel__section-title{font-family:SourceSansProRegular,sans-serif}.theme-george .panel__section-title{font-family:LatoRegular,sans-serif}.panel__sub-title{font-size:12px;letter-spacing:.2px;text-align:left;color:#000}.panel__anchor{display:block;padding:0;float:right}.panel__main{transition:height .2s cubic-bezier(0.4,0,0.2,1);padding:16px}.panel__main .expanded_container{padding:16px;float:left;width:100%}.panel__footer{height:40px;line-height:40px;padding:0 10px}@media (min-width:320px){.panel__anchor{padding:0 10px 0 25px}}@media (min-width:768px){.panel--no-border{border:0}}.button-container{width:100%;height:44px;display:block;margin-top:4px}@media (min-width:320px){.button-container{height:auto}}@media (min-width:481px){.button-container{height:44px}}.button-container .half{margin:0 2% 0 0}@media (min-width:320px){.button-container .half.half{width:100%;margin:0 0 8px}}@media (min-width:481px){.button-container .half.half{width:calc(50% - 8px);float:right;margin-right:8px}.button-container .half.half:nth-child(1){margin:0 0 0 2%}}h4.address-form-title{padding-bottom:12px}.theme-ghs h4.address-form-title{font-family:SourceSansProSemiBold,sans-serif}.theme-george h4.address-form-title{font-family:LatoRegular,sans-serif}h4.address-form-title button{height:20px;padding:0;font-size:14px;text-transform:none}.link-toggle{border:0;padding:0;text-align:left;font-size:14px;width:auto;height:auto;margin:0}.theme-ghs .link-toggle{font-family:SourceSansProRegular,sans-serif;color:#0073b1;text-decoration:none;letter-spacing:normal}.theme-george .link-toggle{font-family:LatoBold,sans-serif;color:#191919;text-decoration:underline;letter-spacing:normal}.link-toggle:hover{text-decoration:underline}label.radio-group-label{font-size:16px;width:100%;display:block;padding-bottom:12px}.theme-ghs label.radio-group-label{font-family:SourceSansProRegular,sans-serif}.theme-george label.radio-group-label{font-family:LatoRegular,sans-serif}.theme-ghs .find-button{margin-bottom:16px}.theme-george .find-button{margin-bottom:16px}.select-box{position:relative}.select-box label{padding-bottom:4px;display:block}.theme-ghs .select-box label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em;padding:0 0 4px}.theme-george .select-box label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px;padding:0 0 8px}.select-box .select-arrow{position:absolute;top:36px;right:12px;z-index:-100}.theme-ghs .select-box .select-arrow{top:36px}.theme-george .select-box .select-arrow{top:41px}.theme-ghs .select-box .select-arrow.no-label{top:18px}.theme-george .select-box .select-arrow.no-label{top:18px}.select-box select{width:100%;height:40px;font-size:1em;color:#767676;border-radius:4px;border:1px solid #ccc;clear:both;appearance:none;-webkit-appearance:none;cursor:pointer;background-color:transparent;padding:0 30px 0 12px;margin-bottom:8px;transition:all 200ms ease}.theme-ghs .select-box select{height:40px;font-size:1em;margin-bottom:12px;font-family:SourceSansProRegular,sans-serif;color:#3d3d3d}.theme-george .select-box select{height:42px;font-size:.875em;margin-bottom:16px;font-family:LatoRegular,sans-serif;color:#191919}.select-box select:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.select-box select:focus{border-color:#767676}.select-box select.half{width:calc(50% - 10px);float:left;margin-right:8px}.select-box select.error{border:2px solid #d43030;margin-bottom:0}.select-box.half{width:calc(50% - 5px);float:left;margin-right:10px}.select-box.no-gutter{margin:0}.select-error{margin-bottom:8px;margin-top:4px}.text-area label{padding-bottom:4px;display:block}.theme-ghs .text-area label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em}.theme-george .text-area label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px}.text-area textarea{width:100%;height:110px;font-size:1em;border-radius:4px;border:1px solid #ccc;clear:both;padding:8px;margin-bottom:8px;color:#191919;font-family:SourceSansProRegular,sans-serif;resize:none}.text-area textarea::-webkit-input-placeholder{color:#767676}.text-area textarea:-moz-placeholder{color:#767676}.text-area textarea::-moz-placeholder{color:#767676}.text-area textarea:-ms-input-placeholder{color:#767676}.text-area textarea.half{width:calc(50% - 10px);float:left;margin-right:8px}.text-area textarea.error{border:solid 1px #d43030;margin-bottom:0}.text-area.half{width:calc(50% - 5px);float:left;margin-right:10px}.text-area.no-gutter{margin:0}.input-error{margin-bottom:8px;margin-top:4px}.co-account__title{font-size:22px;padding-bottom:8px}.theme-ghs .co-account__title{font-family:SourceSansProRegular,sans-serif;padding-bottom:8px}.theme-george .co-account__title{font-family:LatoRegular,sans-serif;padding-bottom:32px}.co-account__description{font-size:14px;padding-bottom:16px}.theme-ghs .co-account__description{font-family:SourceSansProRegular,sans-serif}.theme-george .co-account__description{font-family:LatoRegular,sans-serif}a.george-account-link{font-family:LatoRegular,sans-serif;font-size:14px;color:#191919;display:block}a.george-account-link img{margin-right:8px}.edit-link{position:absolute;right:0;top:22px;padding:0;border:0;width:auto;height:auto;margin:0;font-size:18px;background:transparent;transition:all 200ms ease;letter-spacing:normal}.theme-ghs .edit-link{padding:0;height:auto;text-transform:none}.theme-george .edit-link{padding:0;height:auto;text-transform:none}.edit-link:hover{text-decoration:underline}.edit-link.change{top:16px}.theme-ghs .edit-link{font-family:SourceSansProRegular,sans-serif;color:#0073b1;text-decoration:none}.theme-george .edit-link{font-family:LatoBold,sans-serif;color:#191919;text-decoration:underline}.dropdown-list-item.selected,.dropdown-list-item:hover{color:#fff;background-color:#767676}.dropdown-wrapper-single{user-select:none;position:relative;width:265px}.dropdown-wrapper-single .dropdown-header{border:1px solid #ccc}.dropdown-wrapper-single .dropdown-header .dropdown-header-name{font-weight:400}.dropdown-wrapper-single .dropdown-list{border:1px solid #ccc;border-top:0}.dropdown-wrapper{font-family:SourceSansProRegular,sans-serif;font-size:24px;font-weight:600;font-style:normal;font-stretch:normal;line-height:normal;letter-spacing:.3px;float:left;margin-right:16px}.dropdown-wrapper .dropdown-header{display:flex;align-items:center;justify-content:space-between;width:176px;height:78px;border:1px solid #767676;cursor:default;position:relative;background-color:#fff;padding:20px}.dropdown-wrapper .dropdown-header.active{border-bottom:0px;top:1px;z-index:11}.dropdown-wrapper .list-wrapper .dropdown-list{z-index:10;position:absolute;width:560px;height:636px;border:1px solid #767676;background-color:#fff;padding:20px 75px 20px 20px;overflow-y:scroll;color:#767676}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{width:100%;padding:8px 0px;line-height:1.54px;cursor:default;display:flex;align-items:center;justify-content:space-between;text-overflow:ellipsis}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:nth-child(5){padding-bottom:18px;border-bottom:2px solid #979797}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px;text-align:left;padding-left:15px}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px;background-color:none}.dropdown-wrapper .dropdown-list::-webkit-scrollbar-thumb{background-image:var(--sf-img-15);height:109px;background-repeat:no-repeat}.dropdown-wrapper .dropdown-list-item.selected,.dropdown-wrapper .dropdown-list-item:hover{color:#fff;background-color:#767676}div[class^=country-flag-]{width:34px;height:20px;background-size:34px 20px}div[class^=country-flag-] img{width:34px;height:20px;background-repeat:no-repeat;background-size:34px 20px}.list{font-family:SourceSansProRegular,sans-serif;font-size:14px;padding:8px 0 16px}.list-header{width:100%;padding-bottom:16px}.list-header-name{float:left;width:68%;margin-right:5%;font-family:SourceSansProSemiBold,sans-serif}.list-header-default{float:left;color:gray}.list-item{width:100%;height:auto;min-height:48px;border-bottom:1px solid #ccc;padding:0}.list-item.editing{border-bottom:0}.list-item-name{width:68%;text-overflow:ellipsis;float:left;white-space:nowrap;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;margin-right:6%;padding:16px 0 0}.list-item-radio{clear:none;float:left;margin-top:13px}.list-item-edit-button{display:block;width:auto;height:auto;border:0;padding:0;color:#000;font-family:SourceSansProRegular,sans-serif;font-size:14px;clear:both}.theme-ghs .list-item-edit-button{height:auto;padding:0;margin:0 0 16px 42px;font-size:14px;text-transform:none;letter-spacing:normal;text-align:left;cursor:pointer;font-family:SourceSansProRegular,sans-serif;color:#0073b1;text-decoration:none}.theme-george .list-item-edit-button{height:auto;padding:0;margin:0 0 16px 42px;font-size:14px;text-transform:none;letter-spacing:normal;text-align:left;cursor:pointer;font-family:LatoRegular,sans-serif;color:#191919;text-decoration:underline}.list-item-edit-button:hover{text-decoration:underline}.list-item-delete-button{display:block;width:auto;border:0;float:right;color:#000;font-family:SourceSansProRegular,sans-serif;font-size:14px}.theme-ghs .list-item-delete-button{height:auto;padding:0;margin:0;font-size:14px;text-transform:none;letter-spacing:normal;font-family:SourceSansProRegular,sans-serif;text-decoration:underline;cursor:pointer}.theme-george .list-item-delete-button{height:auto;padding:0;margin:0;font-size:14px;text-transform:none;letter-spacing:normal;font-family:LatoRegular,sans-serif;text-decoration:underline;cursor:pointer}.list-item-delete-button:hover{text-decoration:underline}.list-item-delete-button img{margin-top:6px;float:right}.phone-input{width:100%}@media (min-width:320px){.phone-input{display:flex;flex-direction:column}}@media (min-width:481px){.phone-input{flex-direction:row;height:52px}}.phone-input .dropdown-wrapper{float:left;height:42px;margin-right:8px;color:#3d3d3d;font-family:SourceSansProRegular,sans-serif;transition:all 200ms ease;font-size:1em;user-select:none}@media (min-width:320px){.phone-input .dropdown-wrapper{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper{width:32%}}.phone-input .dropdown-wrapper .dropdown-header{cursor:pointer;width:100%;height:40px;padding:0 8px;border-radius:4px;border:1px solid #ccc;transition:box-shadow 200ms ease}.phone-input .dropdown-wrapper .dropdown-header.active{height:41px;border-top-color:#767676;border-right-color:#767676;border-left-color:#767676;border-bottom-left-radius:0;border-bottom-right-radius:0;top:0}.phone-input .dropdown-wrapper .dropdown-header.active .country-code{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active div[class^=country-flag-] img{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active .down-arrow{transform:rotate(0.5turn);padding-bottom:4px}.phone-input .dropdown-wrapper .dropdown-header:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.phone-input .dropdown-wrapper .dropdown-header .country-code{font-size:16px;padding-left:4px}@media (min-width:320px){.phone-input .dropdown-wrapper .dropdown-header .country-code{flex:1;padding-left:15px;display:flex;align-items:center}}.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{width:100px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:215px}@media (min-width:481px){.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{display:none}}.phone-input .dropdown-wrapper .dropdown-header .down-arrow{width:20px;padding-top:4px}.phone-input .dropdown-wrapper .dropdown-header div[class^=country-flag-] img{width:32px;background-repeat:no-repeat;background-size:34px 20px}.phone-input .dropdown-wrapper .list-wrapper{position:relative}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{margin-top:-1px;z-index:10;position:absolute;height:200px;border:1px solid #767676;border-radius:0 0 4px 4px;background-color:#fff;padding:0;overflow-y:scroll;color:#767676}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:372px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list::-webkit-scrollbar *{background:transparent}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{color:#191919;height:40px;font-size:1em}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:hover{background-color:#f6f6f6}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 8px 12px 8px;height:48px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 80px 12px 8px;height:40px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{line-height:initial}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:calc(100% - 32px)}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{text-align:right}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:60px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px}}@media (min-width:320px){.phone-input .input-box{width:100%;margin-top:12px}}@media (min-width:481px){.phone-input .input-box{width:calc(68% - 8px);margin-top:0px}}.card-name{line-height:initial}.card-name .custom-icon:before{content:"";width:30px;margin-right:12px;margin-top:0px;height:20px;float:left;background-image:var(--sf-img-16)}.card-name .custom-icon.amex:before{background-image:var(--sf-img-17)}.card-name .custom-icon.maestro:before{background-image:var(--sf-img-18)}.card-name .custom-icon.mastercard:before{background-image:var(--sf-img-19)}.card-name .custom-icon.visa:before{background-image:var(--sf-img-20)}.card-name .card-info{font-size:12px;clear:both;padding-left:42px;padding-bottom:12px;color:#767676}.card-name .card-info .highlight-red{color:#d43030}.modal{position:absolute;z-index:1000;top:8px;left:8px;display:block;width:536px;max-width:calc(100% - 16px);height:auto;border-radius:8px;box-shadow:2px 3px 8px 3px rgba(0,0,0,.1);border:solid 1px #ccc;background-color:#fff;font-style:normal;font-stretch:normal;line-height:normal;color:#3d3d3d}.theme-ghs .modal{font-family:SourceSansProRegular,sans-serif}.theme-george .modal{font-family:LatoRegular,sans-serif}.modal-title{font-size:22px;padding:16px;border-bottom:solid 1px #ccc}.theme-ghs .modal-title{font-family:SourceSansProSemiBold,sans-serif}.theme-george .modal-title{font-family:LatoRegular,sans-serif}.modal-content{font-size:16px;padding:16px;border-bottom:solid 1px #ccc}.modal-footer{display:flex;justify-content:center;padding:16px}.modal-footer .half{width:45%}.modal-footer .full{width:100%}.input-error.server-error{margin:0 0 12px}.theme-ghs .help-icon{width:auto;height:auto;border:0;padding:0;margin:-7px 0 0 4px;float:left}.theme-george .help-icon{width:auto;height:auto;border:0;padding:0;margin:-7px 0 0 4px;float:left}.bubble{position:absolute;top:100px;left:16px;width:288px;height:auto;background-color:#191919;color:#fff;padding:12px 30px 12px 12px;border-radius:8px;font-size:14px;z-index:1000;font-family:SourceSansProRegular,sans-serif;box-shadow:0 2px 10px 0 rgba(0,0,0,.2);line-height:18px}.bubble .close-icon{position:absolute;top:8px;right:8px;width:auto;height:auto;border:0;padding:0;margin:0;background:transparent}.bubble .triangle{width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent;border-bottom:10px solid #191919;left:20px;top:-10px;position:absolute}.alert-box{display:flex;flex-direction:row;width:auto;height:auto;border-radius:4px;background-color:rgba(247,204,0,.2);margin-bottom:16px;padding:10px 24px 10px 8px}.alert-box img{width:24px;height:24px;object-fit:contain}.alert-box .alert-message{padding:0;margin-left:8px;font-size:16px;color:#3d3d3d}.divider{width:100%;border-top:1px solid #cbcbcb;margin-bottom:24px}</style><meta http-equiv="origin-trial" content="A3v9QjmVUCOO7YqFMKHP/NKbn6kY1G1pa2S1TfeXJZUD/tysMONTy6lV0Jkou3rrCjSKRGbqTrgTaZkm1XJ7pQUAAACKeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZXRhZ21hbmFnZXIuY29tOjQ0MyIsImZlYXR1cmUiOiJDb252ZXJzaW9uTWVhc3VyZW1lbnQiLCJleHBpcnkiOjE2NDMxNTUxOTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9"><style id="onetrust-style">#onetrust-banner-sdk{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}#onetrust-banner-sdk .onetrust-vendors-list-handler{cursor:pointer;color:#1f96db;font-size:inherit;font-weight:bold;text-decoration:none;margin-left:5px}#onetrust-banner-sdk .onetrust-vendors-list-handler:hover{color:#1f96db}#onetrust-banner-sdk:focus{outline:2px solid #000;outline-offset:-2px}#onetrust-banner-sdk a:focus{outline:2px solid #000}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{outline-offset:1px}#onetrust-banner-sdk .ot-close-icon,#onetrust-pc-sdk .ot-close-icon,#ot-sync-ntfy .ot-close-icon{background-image:url("resources/sample/10.svg");background-size:contain;background-repeat:no-repeat;background-position:center;height:12px;width:12px}#onetrust-banner-sdk .powered-by-logo,#onetrust-banner-sdk .ot-pc-footer-logo a,#onetrust-pc-sdk .powered-by-logo,#onetrust-pc-sdk .ot-pc-footer-logo a,#ot-sync-ntfy .powered-by-logo,#ot-sync-ntfy .ot-pc-footer-logo a{background-size:contain;background-repeat:no-repeat;background-position:center;height:25px;width:152px;display:block}#onetrust-banner-sdk h3 *,#onetrust-banner-sdk h4 *,#onetrust-banner-sdk h6 *,#onetrust-banner-sdk button *,#onetrust-banner-sdk a[data-parent-id] *,#onetrust-pc-sdk h3 *,#onetrust-pc-sdk h4 *,#onetrust-pc-sdk h6 *,#onetrust-pc-sdk button *,#onetrust-pc-sdk a[data-parent-id] *,#ot-sync-ntfy h3 *,#ot-sync-ntfy h4 *,#ot-sync-ntfy h6 *,#ot-sync-ntfy button *,#ot-sync-ntfy a[data-parent-id] *{font-size:inherit;font-weight:inherit;color:inherit}#onetrust-banner-sdk .ot-hide,#onetrust-pc-sdk .ot-hide,#ot-sync-ntfy .ot-hide{display:none!important}#onetrust-pc-sdk .ot-sdk-row .ot-sdk-column{padding:0}#onetrust-pc-sdk .ot-sdk-container{padding-right:0}#onetrust-pc-sdk .ot-sdk-row{flex-direction:initial;width:100%}#onetrust-pc-sdk [type="checkbox"]:checked,#onetrust-pc-sdk [type="checkbox"]:not(:checked){pointer-events:initial}#onetrust-pc-sdk [type="checkbox"]:disabled+label::before,#onetrust-pc-sdk [type="checkbox"]:disabled+label:after,#onetrust-pc-sdk [type="checkbox"]:disabled+label{pointer-events:none;opacity:0.7}#onetrust-pc-sdk #vendor-list-content{transform:translate3d(0,0,0)}#onetrust-pc-sdk li input[type="checkbox"]{z-index:1}#onetrust-pc-sdk li .ot-checkbox label{z-index:2}#onetrust-pc-sdk li .ot-checkbox input[type="checkbox"]{height:auto;width:auto}#onetrust-pc-sdk li .host-title a,#onetrust-pc-sdk li .ot-host-name a,#onetrust-pc-sdk li .accordion-text,#onetrust-pc-sdk li .ot-acc-txt{z-index:2;position:relative}#onetrust-pc-sdk input{margin:3px 0.1ex}#onetrust-pc-sdk .pc-logo,#onetrust-pc-sdk .ot-pc-logo{height:60px;width:180px;background-position:center;background-size:contain;background-repeat:no-repeat}#onetrust-pc-sdk .screen-reader-only,#onetrust-pc-sdk .ot-scrn-rdr,.ot-sdk-cookie-policy .screen-reader-only,.ot-sdk-cookie-policy .ot-scrn-rdr{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}#onetrust-pc-sdk.ot-fade-in,.onetrust-pc-dark-filter.ot-fade-in,#onetrust-banner-sdk.ot-fade-in{animation-name:onetrust-fade-in;animation-duration:400ms;animation-timing-function:ease-in-out}#onetrust-pc-sdk.ot-hide{display:none!important}.onetrust-pc-dark-filter.ot-hide{display:none!important}#ot-sdk-btn.ot-sdk-show-settings,#ot-sdk-btn.optanon-show-settings{color:#68b631;border:1px solid #68b631;height:auto;white-space:normal;word-wrap:break-word;padding:0.8em 2em;font-size:0.8em;line-height:1.2;cursor:pointer;-moz-transition:0.1s ease;-o-transition:0.1s ease;-webkit-transition:1s ease;transition:0.1s ease}#ot-sdk-btn.ot-sdk-show-settings:hover,#ot-sdk-btn.optanon-show-settings:hover{color:#fff;background-color:#68b631}.onetrust-pc-dark-filter{background:rgba(0,0,0,0.5);z-index:2147483646;width:100%;height:100%;overflow:hidden;position:fixed;top:0;bottom:0;left:0}@keyframes onetrust-fade-in{0%{opacity:0}100%{opacity:1}}.ot-cookie-label{text-decoration:underline}@media only screen and (min-width:426px) and (max-width:896px) and (orientation:landscape){#onetrust-pc-sdk p{font-size:0.75em}}#onetrust-banner-sdk .banner-option-input:focus+label{outline:1px solid #000;outline-style:auto}.category-vendors-list-handler+a:focus,.category-vendors-list-handler+a:focus-visible{outline:2px solid #000}#onetrust-banner-sdk,#onetrust-pc-sdk,#ot-sdk-cookie-policy,#ot-sync-ntfy{font-size:16px}#onetrust-banner-sdk *,#onetrust-banner-sdk ::after,#onetrust-banner-sdk ::before,#onetrust-pc-sdk *,#onetrust-pc-sdk ::after,#onetrust-pc-sdk ::before,#ot-sdk-cookie-policy *,#ot-sdk-cookie-policy ::after,#ot-sdk-cookie-policy ::before,#ot-sync-ntfy *,#ot-sync-ntfy ::after,#ot-sync-ntfy ::before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}#onetrust-banner-sdk div,#onetrust-banner-sdk span,#onetrust-banner-sdk h1,#onetrust-banner-sdk h2,#onetrust-banner-sdk h3,#onetrust-banner-sdk h4,#onetrust-banner-sdk h5,#onetrust-banner-sdk h6,#onetrust-banner-sdk p,#onetrust-banner-sdk img,#onetrust-banner-sdk svg,#onetrust-banner-sdk button,#onetrust-banner-sdk section,#onetrust-banner-sdk a,#onetrust-banner-sdk label,#onetrust-banner-sdk input,#onetrust-banner-sdk ul,#onetrust-banner-sdk li,#onetrust-banner-sdk nav,#onetrust-banner-sdk table,#onetrust-banner-sdk thead,#onetrust-banner-sdk tr,#onetrust-banner-sdk td,#onetrust-banner-sdk tbody,#onetrust-banner-sdk .ot-main-content,#onetrust-banner-sdk .ot-toggle,#onetrust-banner-sdk #ot-content,#onetrust-banner-sdk #ot-pc-content,#onetrust-banner-sdk .checkbox,#onetrust-pc-sdk div,#onetrust-pc-sdk span,#onetrust-pc-sdk h1,#onetrust-pc-sdk h2,#onetrust-pc-sdk h3,#onetrust-pc-sdk h4,#onetrust-pc-sdk h5,#onetrust-pc-sdk h6,#onetrust-pc-sdk p,#onetrust-pc-sdk img,#onetrust-pc-sdk svg,#onetrust-pc-sdk button,#onetrust-pc-sdk section,#onetrust-pc-sdk a,#onetrust-pc-sdk label,#onetrust-pc-sdk input,#onetrust-pc-sdk ul,#onetrust-pc-sdk li,#onetrust-pc-sdk nav,#onetrust-pc-sdk table,#onetrust-pc-sdk thead,#onetrust-pc-sdk tr,#onetrust-pc-sdk td,#onetrust-pc-sdk tbody,#onetrust-pc-sdk .ot-main-content,#onetrust-pc-sdk .ot-toggle,#onetrust-pc-sdk #ot-content,#onetrust-pc-sdk #ot-pc-content,#onetrust-pc-sdk .checkbox,#ot-sdk-cookie-policy div,#ot-sdk-cookie-policy span,#ot-sdk-cookie-policy h1,#ot-sdk-cookie-policy h2,#ot-sdk-cookie-policy h3,#ot-sdk-cookie-policy h4,#ot-sdk-cookie-policy h5,#ot-sdk-cookie-policy h6,#ot-sdk-cookie-policy p,#ot-sdk-cookie-policy img,#ot-sdk-cookie-policy svg,#ot-sdk-cookie-policy button,#ot-sdk-cookie-policy section,#ot-sdk-cookie-policy a,#ot-sdk-cookie-policy label,#ot-sdk-cookie-policy input,#ot-sdk-cookie-policy ul,#ot-sdk-cookie-policy li,#ot-sdk-cookie-policy nav,#ot-sdk-cookie-policy table,#ot-sdk-cookie-policy thead,#ot-sdk-cookie-policy tr,#ot-sdk-cookie-policy td,#ot-sdk-cookie-policy tbody,#ot-sdk-cookie-policy .ot-main-content,#ot-sdk-cookie-policy .ot-toggle,#ot-sdk-cookie-policy #ot-content,#ot-sdk-cookie-policy #ot-pc-content,#ot-sdk-cookie-policy .checkbox,#ot-sync-ntfy div,#ot-sync-ntfy span,#ot-sync-ntfy h1,#ot-sync-ntfy h2,#ot-sync-ntfy h3,#ot-sync-ntfy h4,#ot-sync-ntfy h5,#ot-sync-ntfy h6,#ot-sync-ntfy p,#ot-sync-ntfy img,#ot-sync-ntfy svg,#ot-sync-ntfy button,#ot-sync-ntfy section,#ot-sync-ntfy a,#ot-sync-ntfy label,#ot-sync-ntfy input,#ot-sync-ntfy ul,#ot-sync-ntfy li,#ot-sync-ntfy nav,#ot-sync-ntfy table,#ot-sync-ntfy thead,#ot-sync-ntfy tr,#ot-sync-ntfy td,#ot-sync-ntfy tbody,#ot-sync-ntfy .ot-main-content,#ot-sync-ntfy .ot-toggle,#ot-sync-ntfy #ot-content,#ot-sync-ntfy #ot-pc-content,#ot-sync-ntfy .checkbox{font-family:inherit;font-weight:normal;-webkit-font-smoothing:auto;letter-spacing:normal;line-height:normal;padding:0;margin:0;height:auto;min-height:0;max-height:none;width:auto;min-width:0;max-width:none;border-radius:0;border:none;clear:none;float:none;position:static;bottom:auto;left:auto;right:auto;top:auto;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;white-space:normal;background:none;overflow:visible;vertical-align:baseline;visibility:visible;z-index:auto;box-shadow:none}#onetrust-banner-sdk label:before,#onetrust-banner-sdk label:after,#onetrust-banner-sdk .checkbox:after,#onetrust-banner-sdk .checkbox:before,#onetrust-pc-sdk label:before,#onetrust-pc-sdk label:after,#onetrust-pc-sdk .checkbox:after,#onetrust-pc-sdk .checkbox:before,#ot-sdk-cookie-policy label:before,#ot-sdk-cookie-policy label:after,#ot-sdk-cookie-policy .checkbox:after,#ot-sdk-cookie-policy .checkbox:before,#ot-sync-ntfy label:before,#ot-sync-ntfy label:after,#ot-sync-ntfy .checkbox:after,#ot-sync-ntfy .checkbox:before{content:"";content:none}#onetrust-banner-sdk .ot-sdk-container,#onetrust-pc-sdk .ot-sdk-container,#ot-sdk-cookie-policy .ot-sdk-container{position:relative;width:100%;max-width:100%;margin:0 auto;padding:0 20px;box-sizing:border-box}#onetrust-banner-sdk .ot-sdk-column,#onetrust-banner-sdk .ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-column,#onetrust-pc-sdk .ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-column,#ot-sdk-cookie-policy .ot-sdk-columns{width:100%;float:left;box-sizing:border-box;padding:0;display:initial}@media (min-width:400px){#onetrust-banner-sdk .ot-sdk-container,#onetrust-pc-sdk .ot-sdk-container,#ot-sdk-cookie-policy .ot-sdk-container{width:90%;padding:0}}@media (min-width:550px){#onetrust-banner-sdk .ot-sdk-container,#onetrust-pc-sdk .ot-sdk-container,#ot-sdk-cookie-policy .ot-sdk-container{width:100%}#onetrust-banner-sdk .ot-sdk-column,#onetrust-banner-sdk .ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-column,#onetrust-pc-sdk .ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-column,#ot-sdk-cookie-policy .ot-sdk-columns{margin-left:4%}#onetrust-banner-sdk .ot-sdk-column:first-child,#onetrust-banner-sdk .ot-sdk-columns:first-child,#onetrust-pc-sdk .ot-sdk-column:first-child,#onetrust-pc-sdk .ot-sdk-columns:first-child,#ot-sdk-cookie-policy .ot-sdk-column:first-child,#ot-sdk-cookie-policy .ot-sdk-columns:first-child{margin-left:0}#onetrust-banner-sdk .ot-sdk-two.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-two.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-two.ot-sdk-columns{width:13.3333333333%}#onetrust-banner-sdk .ot-sdk-three.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-three.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-three.ot-sdk-columns{width:22%}#onetrust-banner-sdk .ot-sdk-four.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-four.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-four.ot-sdk-columns{width:30.6666666667%}#onetrust-banner-sdk .ot-sdk-eight.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-eight.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-eight.ot-sdk-columns{width:65.3333333333%}#onetrust-banner-sdk .ot-sdk-nine.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-nine.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-nine.ot-sdk-columns{width:74%}#onetrust-banner-sdk .ot-sdk-ten.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-ten.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-ten.ot-sdk-columns{width:82.6666666667%}#onetrust-banner-sdk .ot-sdk-eleven.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-eleven.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-eleven.ot-sdk-columns{width:91.3333333333%}#onetrust-banner-sdk .ot-sdk-twelve.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-twelve.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-twelve.ot-sdk-columns{width:100%;margin-left:0}}#onetrust-banner-sdk h1,#onetrust-banner-sdk h2,#onetrust-banner-sdk h3,#onetrust-banner-sdk h4,#onetrust-banner-sdk h5,#onetrust-banner-sdk h6,#onetrust-pc-sdk h1,#onetrust-pc-sdk h2,#onetrust-pc-sdk h3,#onetrust-pc-sdk h4,#onetrust-pc-sdk h5,#onetrust-pc-sdk h6,#ot-sdk-cookie-policy h1,#ot-sdk-cookie-policy h2,#ot-sdk-cookie-policy h3,#ot-sdk-cookie-policy h4,#ot-sdk-cookie-policy h5,#ot-sdk-cookie-policy h6{margin-top:0;font-weight:600;font-family:inherit}#onetrust-banner-sdk h1,#onetrust-pc-sdk h1,#ot-sdk-cookie-policy h1{font-size:1.5rem;line-height:1.2}#onetrust-banner-sdk h2,#onetrust-pc-sdk h2,#ot-sdk-cookie-policy h2{font-size:1.5rem;line-height:1.25}#onetrust-banner-sdk h3,#onetrust-pc-sdk h3,#ot-sdk-cookie-policy h3{font-size:1.5rem;line-height:1.3}#onetrust-banner-sdk h4,#onetrust-pc-sdk h4,#ot-sdk-cookie-policy h4{font-size:1.5rem;line-height:1.35}#onetrust-banner-sdk h5,#onetrust-pc-sdk h5,#ot-sdk-cookie-policy h5{font-size:1.5rem;line-height:1.5}#onetrust-banner-sdk h6,#onetrust-pc-sdk h6,#ot-sdk-cookie-policy h6{font-size:1.5rem;line-height:1.6}@media (min-width:550px){#onetrust-banner-sdk h1,#onetrust-pc-sdk h1,#ot-sdk-cookie-policy h1{font-size:1.5rem}#onetrust-banner-sdk h2,#onetrust-pc-sdk h2,#ot-sdk-cookie-policy h2{font-size:1.5rem}#onetrust-banner-sdk h3,#onetrust-pc-sdk h3,#ot-sdk-cookie-policy h3{font-size:1.5rem}#onetrust-banner-sdk h4,#onetrust-pc-sdk h4,#ot-sdk-cookie-policy h4{font-size:1.5rem}#onetrust-banner-sdk h5,#onetrust-pc-sdk h5,#ot-sdk-cookie-policy h5{font-size:1.5rem}#onetrust-banner-sdk h6,#onetrust-pc-sdk h6,#ot-sdk-cookie-policy h6{font-size:1.5rem}}#onetrust-banner-sdk p,#onetrust-pc-sdk p,#ot-sdk-cookie-policy p{margin:0 0 1em 0;font-family:inherit;line-height:normal}#onetrust-banner-sdk a,#onetrust-pc-sdk a,#ot-sdk-cookie-policy a{color:#565656;text-decoration:underline}#onetrust-banner-sdk a:hover,#onetrust-pc-sdk a:hover,#ot-sdk-cookie-policy a:hover{color:#565656;text-decoration:none}#onetrust-banner-sdk .ot-sdk-button,#onetrust-banner-sdk button,#onetrust-pc-sdk .ot-sdk-button,#onetrust-pc-sdk button,#ot-sdk-cookie-policy .ot-sdk-button,#ot-sdk-cookie-policy button{margin-bottom:1rem;font-family:inherit}#onetrust-banner-sdk .ot-sdk-button,#onetrust-banner-sdk button,#onetrust-pc-sdk .ot-sdk-button,#onetrust-pc-sdk button,#ot-sdk-cookie-policy .ot-sdk-button,#ot-sdk-cookie-policy button{display:inline-block;height:38px;padding:0 30px;color:#555;text-align:center;font-size:0.9em;font-weight:400;line-height:38px;letter-spacing:0.01em;text-decoration:none;white-space:nowrap;background-color:transparent;border-radius:2px;border:1px solid #bbb;cursor:pointer;box-sizing:border-box}#onetrust-banner-sdk .ot-sdk-button:hover,#onetrust-banner-sdk :not(.ot-leg-btn-container)>button:hover,#onetrust-banner-sdk :not(.ot-leg-btn-container)>button:focus,#onetrust-pc-sdk .ot-sdk-button:hover,#onetrust-pc-sdk :not(.ot-leg-btn-container)>button:hover,#onetrust-pc-sdk :not(.ot-leg-btn-container)>button:focus,#ot-sdk-cookie-policy .ot-sdk-button:hover,#ot-sdk-cookie-policy :not(.ot-leg-btn-container)>button:hover,#ot-sdk-cookie-policy :not(.ot-leg-btn-container)>button:focus{color:#333;border-color:#888;opacity:0.7}#onetrust-banner-sdk .ot-sdk-button:focus,#onetrust-banner-sdk :not(.ot-leg-btn-container)>button:focus,#onetrust-pc-sdk .ot-sdk-button:focus,#onetrust-pc-sdk :not(.ot-leg-btn-container)>button:focus,#ot-sdk-cookie-policy .ot-sdk-button:focus,#ot-sdk-cookie-policy :not(.ot-leg-btn-container)>button:focus{outline:2px solid #000}#onetrust-banner-sdk .ot-sdk-button.ot-sdk-button-primary,#onetrust-banner-sdk button.ot-sdk-button-primary,#onetrust-banner-sdk input[type="submit"].ot-sdk-button-primary,#onetrust-banner-sdk input[type="reset"].ot-sdk-button-primary,#onetrust-banner-sdk input[type="button"].ot-sdk-button-primary,#onetrust-pc-sdk .ot-sdk-button.ot-sdk-button-primary,#onetrust-pc-sdk button.ot-sdk-button-primary,#onetrust-pc-sdk input[type="submit"].ot-sdk-button-primary,#onetrust-pc-sdk input[type="reset"].ot-sdk-button-primary,#onetrust-pc-sdk input[type="button"].ot-sdk-button-primary,#ot-sdk-cookie-policy .ot-sdk-button.ot-sdk-button-primary,#ot-sdk-cookie-policy button.ot-sdk-button-primary,#ot-sdk-cookie-policy input[type="submit"].ot-sdk-button-primary,#ot-sdk-cookie-policy input[type="reset"].ot-sdk-button-primary,#ot-sdk-cookie-policy input[type="button"].ot-sdk-button-primary{color:#fff;background-color:#33c3f0;border-color:#33c3f0}#onetrust-banner-sdk .ot-sdk-button.ot-sdk-button-primary:hover,#onetrust-banner-sdk button.ot-sdk-button-primary:hover,#onetrust-banner-sdk input[type="submit"].ot-sdk-button-primary:hover,#onetrust-banner-sdk input[type="reset"].ot-sdk-button-primary:hover,#onetrust-banner-sdk input[type="button"].ot-sdk-button-primary:hover,#onetrust-banner-sdk .ot-sdk-button.ot-sdk-button-primary:focus,#onetrust-banner-sdk button.ot-sdk-button-primary:focus,#onetrust-banner-sdk input[type="submit"].ot-sdk-button-primary:focus,#onetrust-banner-sdk input[type="reset"].ot-sdk-button-primary:focus,#onetrust-banner-sdk input[type="button"].ot-sdk-button-primary:focus,#onetrust-pc-sdk .ot-sdk-button.ot-sdk-button-primary:hover,#onetrust-pc-sdk button.ot-sdk-button-primary:hover,#onetrust-pc-sdk input[type="submit"].ot-sdk-button-primary:hover,#onetrust-pc-sdk input[type="reset"].ot-sdk-button-primary:hover,#onetrust-pc-sdk input[type="button"].ot-sdk-button-primary:hover,#onetrust-pc-sdk .ot-sdk-button.ot-sdk-button-primary:focus,#onetrust-pc-sdk button.ot-sdk-button-primary:focus,#onetrust-pc-sdk input[type="submit"].ot-sdk-button-primary:focus,#onetrust-pc-sdk input[type="reset"].ot-sdk-button-primary:focus,#onetrust-pc-sdk input[type="button"].ot-sdk-button-primary:focus,#ot-sdk-cookie-policy .ot-sdk-button.ot-sdk-button-primary:hover,#ot-sdk-cookie-policy button.ot-sdk-button-primary:hover,#ot-sdk-cookie-policy input[type="submit"].ot-sdk-button-primary:hover,#ot-sdk-cookie-policy input[type="reset"].ot-sdk-button-primary:hover,#ot-sdk-cookie-policy input[type="button"].ot-sdk-button-primary:hover,#ot-sdk-cookie-policy .ot-sdk-button.ot-sdk-button-primary:focus,#ot-sdk-cookie-policy button.ot-sdk-button-primary:focus,#ot-sdk-cookie-policy input[type="submit"].ot-sdk-button-primary:focus,#ot-sdk-cookie-policy input[type="reset"].ot-sdk-button-primary:focus,#ot-sdk-cookie-policy input[type="button"].ot-sdk-button-primary:focus{color:#fff;background-color:#1eaedb;border-color:#1eaedb}#onetrust-banner-sdk input[type="text"],#onetrust-pc-sdk input[type="text"],#ot-sdk-cookie-policy input[type="text"]{height:38px;padding:6px 10px;background-color:#fff;border:1px solid #d1d1d1;border-radius:4px;box-shadow:none;box-sizing:border-box}#onetrust-banner-sdk input[type="text"],#onetrust-pc-sdk input[type="text"],#ot-sdk-cookie-policy input[type="text"]{-webkit-appearance:none;-moz-appearance:none;appearance:none}#onetrust-banner-sdk input[type="text"]:focus,#onetrust-pc-sdk input[type="text"]:focus,#ot-sdk-cookie-policy input[type="text"]:focus{border:1px solid #000;outline:0}#onetrust-banner-sdk label,#onetrust-pc-sdk label,#ot-sdk-cookie-policy label{display:block;margin-bottom:0.5rem;font-weight:600}#onetrust-banner-sdk input[type="checkbox"],#onetrust-pc-sdk input[type="checkbox"],#ot-sdk-cookie-policy input[type="checkbox"]{display:inline}#onetrust-banner-sdk ul,#onetrust-pc-sdk ul,#ot-sdk-cookie-policy ul{list-style:circle inside}#onetrust-banner-sdk ul,#onetrust-pc-sdk ul,#ot-sdk-cookie-policy ul{padding-left:0;margin-top:0}#onetrust-banner-sdk ul ul,#onetrust-pc-sdk ul ul,#ot-sdk-cookie-policy ul ul{margin:1.5rem 0 1.5rem 3rem;font-size:90%}#onetrust-banner-sdk li,#onetrust-pc-sdk li,#ot-sdk-cookie-policy li{margin-bottom:1rem}#onetrust-banner-sdk th,#onetrust-banner-sdk td,#onetrust-pc-sdk th,#onetrust-pc-sdk td,#ot-sdk-cookie-policy th,#ot-sdk-cookie-policy td{padding:12px 15px;text-align:left;border-bottom:1px solid #e1e1e1}#onetrust-banner-sdk button,#onetrust-pc-sdk button,#ot-sdk-cookie-policy button{margin-bottom:1rem;font-family:inherit}#onetrust-banner-sdk .ot-sdk-container:after,#onetrust-banner-sdk .ot-sdk-row:after,#onetrust-pc-sdk .ot-sdk-container:after,#onetrust-pc-sdk .ot-sdk-row:after,#ot-sdk-cookie-policy .ot-sdk-container:after,#ot-sdk-cookie-policy .ot-sdk-row:after{content:"";display:table;clear:both}#onetrust-banner-sdk .ot-sdk-row,#onetrust-pc-sdk .ot-sdk-row,#ot-sdk-cookie-policy .ot-sdk-row{margin:0;max-width:none;display:block}#onetrust-banner-sdk{box-shadow:0 0 18px rgba(0,0,0,.2)}#onetrust-banner-sdk.otFlat{position:fixed;z-index:2147483645;bottom:0;right:0;left:0;background-color:#fff;max-height:90%;overflow-x:hidden;overflow-y:auto}#onetrust-banner-sdk.otFlat.top{top:0px;bottom:auto}#onetrust-banner-sdk.otRelFont{font-size:1rem}#onetrust-banner-sdk>.ot-sdk-container{overflow:hidden}#onetrust-banner-sdk::-webkit-scrollbar{width:11px}#onetrust-banner-sdk::-webkit-scrollbar-thumb{border-radius:10px;background:#c1c1c1}#onetrust-banner-sdk{scrollbar-arrow-color:#c1c1c1;scrollbar-darkshadow-color:#c1c1c1;scrollbar-face-color:#c1c1c1;scrollbar-shadow-color:#c1c1c1}#onetrust-banner-sdk #onetrust-policy{margin:1.25em 0 .625em 2em;overflow:hidden}#onetrust-banner-sdk #onetrust-policy .ot-gv-list-handler{float:left;font-size:.82em;padding:0;margin-bottom:0;border:0;line-height:normal;height:auto;width:auto}#onetrust-banner-sdk #onetrust-policy-title{font-size:1.2em;line-height:1.3;margin-bottom:10px}#onetrust-banner-sdk #onetrust-policy-text{clear:both;text-align:left;font-size:.88em;line-height:1.4}#onetrust-banner-sdk #onetrust-policy-text *{font-size:inherit;line-height:inherit}#onetrust-banner-sdk #onetrust-policy-text a{font-weight:bold;margin-left:5px}#onetrust-banner-sdk #onetrust-policy-title,#onetrust-banner-sdk #onetrust-policy-text{color:dimgray;float:left}#onetrust-banner-sdk #onetrust-button-group-parent{min-height:1px;text-align:center}#onetrust-banner-sdk #onetrust-button-group{display:inline-block}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{background-color:#68b631;color:#fff;border-color:#68b631;margin-right:1em;min-width:125px;height:auto;white-space:normal;word-break:break-word;word-wrap:break-word;padding:12px 10px;line-height:1.2;font-size:.813em;font-weight:600}#onetrust-banner-sdk #onetrust-pc-btn-handler.cookie-setting-link{background-color:#fff;border:none;color:#68b631;text-decoration:underline;padding-left:0;padding-right:0}#onetrust-banner-sdk .onetrust-close-btn-ui{width:44px;height:44px;background-size:12px;border:none;position:relative;margin:auto;padding:0}#onetrust-banner-sdk .banner_logo{display:none}#onetrust-banner-sdk .ot-b-addl-desc{clear:both;float:left;display:block}#onetrust-banner-sdk #banner-options{float:left;display:table;margin-right:0;margin-left:1em;width:calc(100% - 1em)}#onetrust-banner-sdk .banner-option-input{cursor:pointer;width:auto;height:auto;border:none;padding:0;padding-right:3px;margin:0 0 10px;font-size:.82em;line-height:1.4}#onetrust-banner-sdk .banner-option-input *{pointer-events:none;font-size:inherit;line-height:inherit}#onetrust-banner-sdk .banner-option-input[aria-expanded=true]~.banner-option-details{display:block;height:auto}#onetrust-banner-sdk .banner-option-input[aria-expanded=true] .ot-arrow-container{transform:rotate(90deg)}#onetrust-banner-sdk .banner-option{margin-bottom:12px;margin-left:0;border:none;float:left;padding:0}#onetrust-banner-sdk .banner-option:first-child{padding-left:2px}#onetrust-banner-sdk .banner-option:not(:first-child){padding:0;border:none}#onetrust-banner-sdk .banner-option-header{cursor:pointer;display:inline-block}#onetrust-banner-sdk .banner-option-header :first-child{color:dimgray;font-weight:bold;float:left}#onetrust-banner-sdk .banner-option-header .ot-arrow-container{display:inline-block;border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:6px solid dimgray;margin-left:10px;vertical-align:middle}#onetrust-banner-sdk .banner-option-details{display:none;font-size:.83em;line-height:1.5;padding:10px 0px 5px 10px;margin-right:10px;height:0px}#onetrust-banner-sdk .banner-option-details *{font-size:inherit;line-height:inherit;color:dimgray}#onetrust-banner-sdk .ot-arrow-container,#onetrust-banner-sdk .banner-option-details{transition:all 300ms ease-in 0s;-webkit-transition:all 300ms ease-in 0s;-moz-transition:all 300ms ease-in 0s;-o-transition:all 300ms ease-in 0s}#onetrust-banner-sdk .ot-dpd-container{float:left}#onetrust-banner-sdk .ot-dpd-title{margin-bottom:10px}#onetrust-banner-sdk .ot-dpd-title,#onetrust-banner-sdk .ot-dpd-desc{font-size:.88em;line-height:1.4;color:dimgray}#onetrust-banner-sdk .ot-dpd-title *,#onetrust-banner-sdk .ot-dpd-desc *{font-size:inherit;line-height:inherit}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-text *{margin-bottom:0}#onetrust-banner-sdk.ot-iab-2 .onetrust-vendors-list-handler{display:block;margin-left:0;margin-top:5px;clear:both;margin-bottom:0;padding:0;border:0;height:auto;width:auto}#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group button{display:block}#onetrust-banner-sdk.ot-close-btn-link{padding-top:25px}#onetrust-banner-sdk.ot-close-btn-link #onetrust-close-btn-container{top:15px;transform:none;right:15px}#onetrust-banner-sdk.ot-close-btn-link #onetrust-close-btn-container button{padding:0;white-space:pre-wrap;border:none;height:auto;line-height:1.5;text-decoration:underline;font-size:.69em}#onetrust-banner-sdk #onetrust-policy-text,#onetrust-banner-sdk .ot-dpd-desc,#onetrust-banner-sdk .ot-b-addl-desc{font-size:.813em;line-height:1.5}#onetrust-banner-sdk .ot-dpd-desc{margin-bottom:10px}#onetrust-banner-sdk .ot-dpd-desc>.ot-b-addl-desc{margin-top:10px;margin-bottom:10px;font-size:1em}@media only screen and (max-width:425px){#onetrust-banner-sdk #onetrust-close-btn-container{position:absolute;top:6px;right:2px}#onetrust-banner-sdk #onetrust-policy{margin-left:0;margin-top:3em}#onetrust-banner-sdk #onetrust-button-group{display:block}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{width:100%}#onetrust-banner-sdk .onetrust-close-btn-ui{top:auto;transform:none}#onetrust-banner-sdk #onetrust-policy-title{display:inline;float:none}#onetrust-banner-sdk #banner-options{margin:0;padding:0;width:100%}}@media only screen and (min-width:426px)and (max-width:896px){#onetrust-banner-sdk #onetrust-close-btn-container{position:absolute;top:0;right:0}#onetrust-banner-sdk #onetrust-policy{margin-left:1em;margin-right:1em}#onetrust-banner-sdk .onetrust-close-btn-ui{top:10px;right:10px}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-group-container{width:95%}#onetrust-banner-sdk.ot-iab-2 #onetrust-group-container{width:100%}#onetrust-banner-sdk #onetrust-button-group-parent{width:100%;position:relative;margin-left:0}#onetrust-banner-sdk #onetrust-button-group button{display:inline-block}#onetrust-banner-sdk #onetrust-button-group{margin-right:0;text-align:center}#onetrust-banner-sdk .has-reject-all-button #onetrust-pc-btn-handler{float:left}#onetrust-banner-sdk .has-reject-all-button #onetrust-reject-all-handler,#onetrust-banner-sdk .has-reject-all-button #onetrust-accept-btn-handler{float:right}#onetrust-banner-sdk .has-reject-all-button #onetrust-button-group{width:calc(100% - 2em);margin-right:0}#onetrust-banner-sdk .has-reject-all-button #onetrust-pc-btn-handler.cookie-setting-link{padding-left:0px;text-align:left}#onetrust-banner-sdk.ot-buttons-fw .ot-sdk-three button{width:100%;text-align:center}#onetrust-banner-sdk.ot-buttons-fw #onetrust-button-group-parent button{float:none}#onetrust-banner-sdk.ot-buttons-fw #onetrust-pc-btn-handler.cookie-setting-link{text-align:center}}@media only screen and (min-width:550px){#onetrust-banner-sdk .banner-option:not(:first-child){border-left:1px solid #d8d8d8;padding-left:25px}}@media only screen and (min-width:425px)and (max-width:550px){#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group,#onetrust-banner-sdk.ot-iab-2 #onetrust-policy,#onetrust-banner-sdk.ot-iab-2 .banner-option{width:100%}}@media only screen and (min-width:769px){#onetrust-banner-sdk #onetrust-button-group{margin-right:30%}#onetrust-banner-sdk #banner-options{margin-left:2em;margin-right:5em;margin-bottom:1.25em;width:calc(100% - 7em)}}@media only screen and (min-width:897px)and (max-width:1023px){#onetrust-banner-sdk.vertical-align-content #onetrust-button-group-parent{position:absolute;top:50%;left:75%;transform:translateY(-50%)}#onetrust-banner-sdk #onetrust-close-btn-container{top:50%;margin:auto;transform:translate(-50%,-50%);position:absolute;padding:0;right:0}#onetrust-banner-sdk #onetrust-close-btn-container button{position:relative;margin:0;right:-22px;top:2px}}@media only screen and (min-width:1024px){#onetrust-banner-sdk #onetrust-close-btn-container{top:50%;margin:auto;transform:translate(-50%,-50%);position:absolute;right:0}#onetrust-banner-sdk #onetrust-close-btn-container button{right:-12px}#onetrust-banner-sdk #onetrust-policy{margin-left:2em}#onetrust-banner-sdk.vertical-align-content #onetrust-button-group-parent{position:absolute;top:50%;left:60%;transform:translateY(-50%)}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-title{width:50%}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-text,#onetrust-banner-sdk.ot-iab-2 :not(.ot-dpd-desc)>.ot-b-addl-desc{margin-bottom:1em;width:50%;border-right:1px solid #d8d8d8;padding-right:1rem}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-text{margin-bottom:0;padding-bottom:1em}#onetrust-banner-sdk.ot-iab-2 :not(.ot-dpd-desc)>.ot-b-addl-desc{margin-bottom:0;padding-bottom:1em}#onetrust-banner-sdk.ot-iab-2 .ot-dpd-container{width:45%;padding-left:1rem;display:inline-block;float:none}#onetrust-banner-sdk.ot-iab-2 .ot-dpd-title{line-height:1.7}#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group-parent{left:auto;right:4%;margin-left:0}#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group button{display:block}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-button-group-parent{margin:auto;width:30%}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-group-container{width:60%}#onetrust-banner-sdk #onetrust-button-group{margin-right:auto}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{margin-top:1em}}@media only screen and (min-width:890px){#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group-parent{padding-left:3%;padding-right:4%;margin-left:0}#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group{margin-right:0;margin-top:1.25em;width:100%}#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group button{width:100%;margin-bottom:5px;margin-top:5px}#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group button:last-of-type{margin-bottom:20px}}@media only screen and (min-width:1280px){#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-group-container{width:55%}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-button-group-parent{width:44%;padding-left:2%;padding-right:2%}#onetrust-banner-sdk:not(.ot-iab-2).vertical-align-content #onetrust-button-group-parent{position:absolute;left:55%}}#onetrust-consent-sdk #onetrust-banner-sdk{background-color:#191919}#onetrust-consent-sdk #onetrust-policy-title,#onetrust-consent-sdk #onetrust-policy-text,#onetrust-consent-sdk .ot-b-addl-desc,#onetrust-consent-sdk .ot-dpd-desc,#onetrust-consent-sdk .ot-dpd-title,#onetrust-consent-sdk #onetrust-policy-text *:not(.onetrust-vendors-list-handler),#onetrust-consent-sdk .ot-dpd-desc *:not(.onetrust-vendors-list-handler),#onetrust-consent-sdk #onetrust-banner-sdk #banner-options *,#onetrust-banner-sdk .ot-cat-header{color:#FFFFFF}#onetrust-consent-sdk #onetrust-banner-sdk .banner-option-details{background-color:#E9E9E9}#onetrust-consent-sdk #onetrust-banner-sdk a[href],#onetrust-consent-sdk #onetrust-banner-sdk a[href] font,#onetrust-consent-sdk #onetrust-banner-sdk .ot-link-btn{color:#FFFFFF}#onetrust-consent-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler{background-color:#FDC301;border-color:#FDC301;color:#000000}#onetrust-consent-sdk #onetrust-banner-sdk *:focus,#onetrust-consent-sdk #onetrust-banner-sdk:focus{outline-color:#000000;outline-width:1px}#onetrust-consent-sdk #onetrust-pc-btn-handler,#onetrust-consent-sdk #onetrust-pc-btn-handler.cookie-setting-link{color:#191919;border-color:#191919;background-color:#FDC301}#onetrust-consent-sdk{font-family:Arial,sans-serif}#onetrust-consent-sdk #onetrust-policy-title{font-size:12pt;margin-bottom:5px}#onetrust-consent-sdk #onetrust-policy-text{margin-bottom:0px}#onetrust-consent-sdk #onetrust-pc-btn-handler.cookie-setting-link{color:#fff}@media only screen and (max-width:1024px){#onetrust-banner-sdk #onetrust-policy{margin-top:5px}}@media only screen and (max-width:425px){#onetrust-banner-sdk #onetrust-button-group{display:flex}}#onetrust-pc-sdk.otPcCenter{overflow:hidden;position:fixed;margin:0 auto;top:5%;right:0;left:0;width:40%;max-width:575px;min-width:575px;border-radius:2.5px;z-index:2147483647;background-color:#fff;-webkit-box-shadow:0px 2px 10px -3px #999;-moz-box-shadow:0px 2px 10px -3px #999;box-shadow:0px 2px 10px -3px #999}#onetrust-pc-sdk.otPcCenter[dir=rtl]{right:0;left:0}#onetrust-pc-sdk.otRelFont{font-size:1rem}#onetrust-pc-sdk #ot-addtl-venlst .ot-arw-cntr,#onetrust-pc-sdk #ot-addtl-venlst .ot-plus-minus,#onetrust-pc-sdk .ot-hide-tgl{visibility:hidden}#onetrust-pc-sdk #ot-addtl-venlst .ot-arw-cntr *,#onetrust-pc-sdk #ot-addtl-venlst .ot-plus-minus *,#onetrust-pc-sdk .ot-hide-tgl *{visibility:hidden}#onetrust-pc-sdk #ot-gn-venlst .ot-ven-item .ot-acc-hdr{min-height:40px}#onetrust-pc-sdk .ot-pc-header{height:39px;padding:10px 0 10px 30px;border-bottom:1px solid #e9e9e9}#onetrust-pc-sdk #ot-pc-title,#onetrust-pc-sdk #ot-category-title,#onetrust-pc-sdk .ot-cat-header,#onetrust-pc-sdk #ot-lst-title,#onetrust-pc-sdk .ot-ven-hdr .ot-ven-name,#onetrust-pc-sdk .ot-always-active{font-weight:bold;color:dimgray}#onetrust-pc-sdk .ot-cat-header{float:left;font-weight:600;font-size:.875em;line-height:1.5;max-width:90%;vertical-align:middle}#onetrust-pc-sdk .ot-always-active-group .ot-cat-header{width:55%;font-weight:700}#onetrust-pc-sdk .ot-cat-item p{clear:both;float:left;margin-top:10px;margin-bottom:5px;line-height:1.5;font-size:.812em;color:dimgray}#onetrust-pc-sdk .ot-close-icon{height:44px;width:44px;background-size:10px}#onetrust-pc-sdk #ot-pc-title{float:left;font-size:1em;line-height:1.5;margin-bottom:10px;margin-top:10px;width:100%}#onetrust-pc-sdk #accept-recommended-btn-handler{margin-right:10px;margin-bottom:25px;outline-offset:-1px}#onetrust-pc-sdk #ot-pc-desc{clear:both;width:100%;font-size:.812em;line-height:1.5;margin-bottom:25px}#onetrust-pc-sdk #ot-pc-desc a{margin-left:5px}#onetrust-pc-sdk #ot-pc-desc *{font-size:inherit;line-height:inherit}#onetrust-pc-sdk #ot-pc-desc ul li{padding:10px 0px}#onetrust-pc-sdk a{color:#656565;cursor:pointer}#onetrust-pc-sdk a:hover{color:#3860be}#onetrust-pc-sdk label{margin-bottom:0}#onetrust-pc-sdk #vdr-lst-dsc{font-size:.812em;line-height:1.5;padding:10px 15px 5px 15px}#onetrust-pc-sdk button{max-width:394px;padding:12px 30px;line-height:1;word-break:break-word;word-wrap:break-word;white-space:normal;font-weight:bold;height:auto}#onetrust-pc-sdk .ot-link-btn{padding:0;margin-bottom:0;border:0;font-weight:normal;line-height:normal;width:auto;height:auto}#onetrust-pc-sdk #ot-pc-content{position:absolute;overflow-y:scroll;padding-left:0px;padding-right:30px;top:60px;bottom:110px;margin:1px 3px 0 30px;width:calc(100% - 63px)}#onetrust-pc-sdk .ot-cat-grp .ot-always-active{float:right;clear:none;color:#3860be;margin:0;font-size:.813em;line-height:1.3}#onetrust-pc-sdk .ot-pc-scrollbar::-webkit-scrollbar-track{margin-right:20px}#onetrust-pc-sdk .ot-pc-scrollbar::-webkit-scrollbar{width:11px}#onetrust-pc-sdk .ot-pc-scrollbar::-webkit-scrollbar-thumb{border-radius:10px;background:#d8d8d8}#onetrust-pc-sdk input[type=checkbox]:focus+.ot-acc-hdr{outline:#000 1px solid}#onetrust-pc-sdk .ot-pc-scrollbar{scrollbar-arrow-color:#d8d8d8;scrollbar-darkshadow-color:#d8d8d8;scrollbar-face-color:#d8d8d8;scrollbar-shadow-color:#d8d8d8}#onetrust-pc-sdk .save-preference-btn-handler{margin-right:20px}#onetrust-pc-sdk .ot-pc-refuse-all-handler{margin-right:10px}#onetrust-pc-sdk #ot-pc-desc .privacy-notice-link{margin-left:0}#onetrust-pc-sdk .ot-subgrp-cntr{display:inline-block;clear:both;width:100%;padding-top:15px}#onetrust-pc-sdk .ot-switch+.ot-subgrp-cntr{padding-top:10px}#onetrust-pc-sdk ul.ot-subgrps{margin:0;font-size:initial}#onetrust-pc-sdk ul.ot-subgrps li p,#onetrust-pc-sdk ul.ot-subgrps li h5{font-size:.813em;line-height:1.4;color:dimgray}#onetrust-pc-sdk ul.ot-subgrps .ot-switch{min-height:auto}#onetrust-pc-sdk ul.ot-subgrps .ot-switch-nob{top:0}#onetrust-pc-sdk ul.ot-subgrps .ot-acc-hdr{display:inline-block;width:100%}#onetrust-pc-sdk ul.ot-subgrps .ot-acc-txt{margin:0}#onetrust-pc-sdk ul.ot-subgrps li{padding:0;border:none}#onetrust-pc-sdk ul.ot-subgrps li h5{position:relative;top:5px;font-weight:bold;margin-bottom:0;float:left}#onetrust-pc-sdk li.ot-subgrp{margin-left:20px;overflow:auto}#onetrust-pc-sdk li.ot-subgrp>h5{width:calc(100% - 100px)}#onetrust-pc-sdk .ot-cat-item p>ul,#onetrust-pc-sdk li.ot-subgrp p>ul{margin:0px;list-style:disc;margin-left:15px;font-size:inherit}#onetrust-pc-sdk .ot-cat-item p>ul li,#onetrust-pc-sdk li.ot-subgrp p>ul li{font-size:inherit;padding-top:10px;padding-left:0px;padding-right:0px;border:none}#onetrust-pc-sdk .ot-cat-item p>ul li:last-child,#onetrust-pc-sdk li.ot-subgrp p>ul li:last-child{padding-bottom:10px}#onetrust-pc-sdk .ot-pc-logo{height:40px;width:120px;display:inline-block}#onetrust-pc-sdk .ot-pc-footer{position:absolute;bottom:0px;width:100%;max-height:160px;border-top:1px solid #d8d8d8}#onetrust-pc-sdk.ot-ftr-stacked .ot-pc-refuse-all-handler{margin-bottom:0px}#onetrust-pc-sdk.ot-ftr-stacked #ot-pc-content{bottom:160px}#onetrust-pc-sdk.ot-ftr-stacked .ot-pc-footer button{width:100%;max-width:none}#onetrust-pc-sdk.ot-ftr-stacked .ot-btn-container{margin:0 30px;width:calc(100% - 60px);padding-right:0}#onetrust-pc-sdk .ot-pc-footer-logo{height:30px;width:100%;text-align:right;background:#f4f4f4}#onetrust-pc-sdk .ot-pc-footer-logo a{display:inline-block;margin-top:5px;margin-right:10px}#onetrust-pc-sdk[dir=rtl] .ot-pc-footer-logo{direction:rtl}#onetrust-pc-sdk[dir=rtl] .ot-pc-footer-logo a{margin-right:25px}#onetrust-pc-sdk .ot-tgl{float:right;position:relative;z-index:1}#onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob{background-color:#cddcf2;border:1px solid #3860be}#onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob:before{-webkit-transform:translateX(20px);-ms-transform:translateX(20px);transform:translateX(20px);background-color:#3860be;border-color:#3860be}#onetrust-pc-sdk .ot-tgl input:focus+.ot-switch{outline:#000 solid 1px}#onetrust-pc-sdk .ot-switch{position:relative;display:inline-block;width:45px;height:25px}#onetrust-pc-sdk .ot-switch-nob{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#f2f1f1;border:1px solid #ddd;transition:all .2s ease-in 0s;-moz-transition:all .2s ease-in 0s;-o-transition:all .2s ease-in 0s;-webkit-transition:all .2s ease-in 0s;border-radius:20px}#onetrust-pc-sdk .ot-switch-nob:before{position:absolute;content:"";height:21px;width:21px;bottom:1px;background-color:#7d7d7d;-webkit-transition:.4s;transition:.4s;border-radius:20px}#onetrust-pc-sdk .ot-chkbox input:checked~label::before{background-color:#3860be}#onetrust-pc-sdk .ot-chkbox input+label::after{content:none;color:#fff}#onetrust-pc-sdk .ot-chkbox input:checked+label::after{content:""}#onetrust-pc-sdk .ot-chkbox input:focus+label::before{outline-style:solid;outline-width:2px;outline-style:auto}#onetrust-pc-sdk .ot-chkbox label{position:relative;display:inline-block;padding-left:30px;cursor:pointer;font-weight:500}#onetrust-pc-sdk .ot-chkbox label::before,#onetrust-pc-sdk .ot-chkbox label::after{position:absolute;content:"";display:inline-block;border-radius:3px}#onetrust-pc-sdk .ot-chkbox label::before{height:18px;width:18px;border:1px solid #3860be;left:0px;top:auto}#onetrust-pc-sdk .ot-chkbox label::after{height:5px;width:9px;border-left:3px solid;border-bottom:3px solid;transform:rotate(-45deg);-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);left:4px;top:5px}#onetrust-pc-sdk .ot-label-txt{display:none}#onetrust-pc-sdk .ot-chkbox input,#onetrust-pc-sdk .ot-tgl input{position:absolute;opacity:0;width:0;height:0}#onetrust-pc-sdk .ot-arw-cntr{float:right;position:relative;pointer-events:none}#onetrust-pc-sdk .ot-arw-cntr .ot-arw{width:16px;height:16px;margin-left:5px;color:dimgray;display:inline-block;vertical-align:middle;-webkit-transition:all 150ms ease-in 0s;-moz-transition:all 150ms ease-in 0s;-o-transition:all 150ms ease-in 0s;transition:all 150ms ease-in 0s}#onetrust-pc-sdk input:checked~.ot-acc-hdr .ot-arw,#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-arw-cntr svg{transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);-webkit-transform:rotate(90deg)}#onetrust-pc-sdk input[type=checkbox]:focus+.ot-acc-hdr{outline:#000 1px solid}#onetrust-pc-sdk .ot-tgl-cntr,#onetrust-pc-sdk .ot-arw-cntr{display:inline-block}#onetrust-pc-sdk .ot-tgl-cntr{width:45px;float:right;margin-top:2px}#onetrust-pc-sdk #ot-lst-cnt .ot-tgl-cntr{margin-top:10px}#onetrust-pc-sdk .ot-always-active-subgroup{width:auto;padding-left:0px!important;top:3px;position:relative}#onetrust-pc-sdk .ot-label-status{padding-left:5px;font-size:.75em;display:none}#onetrust-pc-sdk .ot-arw-cntr{margin-top:-1px}#onetrust-pc-sdk .ot-arw-cntr svg{-webkit-transition:all 300ms ease-in 0s;-moz-transition:all 300ms ease-in 0s;-o-transition:all 300ms ease-in 0s;transition:all 300ms ease-in 0s;height:10px;width:10px}#onetrust-pc-sdk input:checked~.ot-acc-hdr .ot-arw{transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);-webkit-transform:rotate(90deg)}#onetrust-pc-sdk .ot-arw{width:10px;margin-left:15px;transition:all 300ms ease-in 0s;-webkit-transition:all 300ms ease-in 0s;-moz-transition:all 300ms ease-in 0s;-o-transition:all 300ms ease-in 0s}#onetrust-pc-sdk .ot-vlst-cntr{margin-bottom:0}#onetrust-pc-sdk .ot-hlst-cntr{margin-top:5px;display:inline-block;width:100%}#onetrust-pc-sdk .category-vendors-list-handler,#onetrust-pc-sdk .category-vendors-list-handler+a,#onetrust-pc-sdk .category-host-list-handler{clear:both;color:#3860be;margin-left:0;font-size:.813em;text-decoration:none;float:left;overflow:hidden}#onetrust-pc-sdk .category-vendors-list-handler:hover,#onetrust-pc-sdk .category-vendors-list-handler+a:hover,#onetrust-pc-sdk .category-host-list-handler:hover{color:#1883fd}#onetrust-pc-sdk .category-vendors-list-handler+a{clear:none}#onetrust-pc-sdk .category-vendors-list-handler+a::after{content:"";height:15px;width:15px;background-repeat:no-repeat;margin-left:5px;float:right;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 511.626 511.627'%3E%3Cg fill='%231276CE'%3E%3Cpath d='M392.857 292.354h-18.274c-2.669 0-4.859.855-6.563 2.573-1.718 1.708-2.573 3.897-2.573 6.563v91.361c0 12.563-4.47 23.315-13.415 32.262-8.945 8.945-19.701 13.414-32.264 13.414H82.224c-12.562 0-23.317-4.469-32.264-13.414-8.945-8.946-13.417-19.698-13.417-32.262V155.31c0-12.562 4.471-23.313 13.417-32.259 8.947-8.947 19.702-13.418 32.264-13.418h200.994c2.669 0 4.859-.859 6.57-2.57 1.711-1.713 2.566-3.9 2.566-6.567V82.221c0-2.662-.855-4.853-2.566-6.563-1.711-1.713-3.901-2.568-6.57-2.568H82.224c-22.648 0-42.016 8.042-58.102 24.125C8.042 113.297 0 132.665 0 155.313v237.542c0 22.647 8.042 42.018 24.123 58.095 16.086 16.084 35.454 24.13 58.102 24.13h237.543c22.647 0 42.017-8.046 58.101-24.13 16.085-16.077 24.127-35.447 24.127-58.095v-91.358c0-2.669-.856-4.859-2.574-6.57-1.713-1.718-3.903-2.573-6.565-2.573z'/%3E%3Cpath d='M506.199 41.971c-3.617-3.617-7.905-5.424-12.85-5.424H347.171c-4.948 0-9.233 1.807-12.847 5.424-3.617 3.615-5.428 7.898-5.428 12.847s1.811 9.233 5.428 12.85l50.247 50.248-186.147 186.151c-1.906 1.903-2.856 4.093-2.856 6.563 0 2.479.953 4.668 2.856 6.571l32.548 32.544c1.903 1.903 4.093 2.852 6.567 2.852s4.665-.948 6.567-2.852l186.148-186.148 50.251 50.248c3.614 3.617 7.898 5.426 12.847 5.426s9.233-1.809 12.851-5.426c3.617-3.616 5.424-7.898 5.424-12.847V54.818c-.001-4.952-1.814-9.232-5.428-12.847z'/%3E%3C/g%3E%3C/svg%3E")}#onetrust-pc-sdk .back-btn-handler{font-size:1em;text-decoration:none}#onetrust-pc-sdk .back-btn-handler:hover{opacity:.6}#onetrust-pc-sdk #ot-lst-title h3{display:inline-block;word-break:break-word;word-wrap:break-word;margin-bottom:0;color:#656565;font-size:1em;font-weight:bold;margin-left:15px}#onetrust-pc-sdk #ot-lst-title{margin:10px 0 10px 0px;font-size:1em;text-align:left}#onetrust-pc-sdk #ot-pc-hdr{margin:0 0 0 30px;height:auto;width:auto}#onetrust-pc-sdk #ot-pc-hdr input::placeholder{color:#d4d4d4;font-style:italic}#onetrust-pc-sdk #vendor-search-handler{height:31px;width:100%;border-radius:50px;font-size:.8em;padding-right:35px;padding-left:15px;float:left;margin-left:15px}#onetrust-pc-sdk .ot-ven-name{display:block;width:auto;padding-right:5px}#onetrust-pc-sdk #ot-lst-cnt{overflow-y:auto;margin-left:20px;margin-right:7px;width:calc(100% - 27px);max-height:calc(100% - 80px);height:100%;transform:translate3d(0,0,0)}#onetrust-pc-sdk #ot-pc-lst{width:100%;bottom:100px;position:absolute;top:60px}#onetrust-pc-sdk #ot-pc-lst:not(.ot-enbl-chr) .ot-tgl-cntr .ot-arw-cntr,#onetrust-pc-sdk #ot-pc-lst:not(.ot-enbl-chr) .ot-tgl-cntr .ot-arw-cntr *{visibility:hidden}#onetrust-pc-sdk #ot-pc-lst .ot-tgl-cntr{right:12px;position:absolute}#onetrust-pc-sdk #ot-pc-lst .ot-arw-cntr{float:right;position:relative}#onetrust-pc-sdk #ot-pc-lst .ot-arw{margin-left:10px}#onetrust-pc-sdk #ot-pc-lst .ot-acc-hdr{overflow:hidden;cursor:pointer}#onetrust-pc-sdk .ot-vlst-cntr{overflow:hidden}#onetrust-pc-sdk #ot-sel-blk{overflow:hidden;width:100%;position:sticky;position:-webkit-sticky;top:0;z-index:3}#onetrust-pc-sdk #ot-back-arw{height:12px;width:12px}#onetrust-pc-sdk .ot-lst-subhdr{width:100%;display:inline-block}#onetrust-pc-sdk .ot-search-cntr{float:left;width:78%;position:relative}#onetrust-pc-sdk .ot-search-cntr>svg{width:30px;height:30px;position:absolute;float:left;right:-15px}#onetrust-pc-sdk .ot-fltr-cntr{float:right;right:50px;position:relative}#onetrust-pc-sdk #filter-btn-handler{background-color:#3860be;border-radius:17px;display:inline-block;position:relative;width:32px;height:32px;-moz-transition:.1s ease;-o-transition:.1s ease;-webkit-transition:1s ease;transition:.1s ease;padding:0;margin:0}#onetrust-pc-sdk #filter-btn-handler:hover{background-color:#3860be}#onetrust-pc-sdk #filter-btn-handler svg{width:12px;height:12px;margin:3px 10px 0 10px;display:block;position:static;right:auto;top:auto}#onetrust-pc-sdk .ot-ven-link{color:#3860be;text-decoration:none;font-weight:100;display:inline-block;padding-top:10px;transform:translate(0,1%);-o-transform:translate(0,1%);-ms-transform:translate(0,1%);-webkit-transform:translate(0,1%);position:relative;z-index:2}#onetrust-pc-sdk .ot-ven-link *{font-size:inherit}#onetrust-pc-sdk .ot-ven-link:hover{text-decoration:underline}#onetrust-pc-sdk .ot-ven-hdr{width:calc(100% - 160px);height:auto;float:left;word-break:break-word;word-wrap:break-word;vertical-align:middle;padding-bottom:3px}#onetrust-pc-sdk .ot-ven-link{letter-spacing:.03em;font-size:.75em;font-weight:400}#onetrust-pc-sdk .ot-ven-dets{border-radius:2px;background-color:#f8f8f8}#onetrust-pc-sdk .ot-ven-dets li:first-child p:first-child{border-top:none}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:not(:first-child){border-top:1px solid #e9e9e9}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:nth-child(n+3) p{display:inline-block}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:nth-child(n+3) p:nth-of-type(odd){width:30%}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:nth-child(n+3) p:nth-of-type(even){width:50%;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc p,#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc h4{padding-top:5px;padding-bottom:5px;display:block}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc h4{display:inline-block}#onetrust-pc-sdk .ot-ven-dets p,#onetrust-pc-sdk .ot-ven-dets h4,#onetrust-pc-sdk .ot-ven-dets span{font-size:.69em;text-align:left;vertical-align:middle;word-break:break-word;word-wrap:break-word;margin:0;padding-bottom:10px;padding-left:15px;color:#2e3644}#onetrust-pc-sdk .ot-ven-dets h4{padding-top:5px}#onetrust-pc-sdk .ot-ven-dets span{color:dimgray;padding:0;vertical-align:baseline}#onetrust-pc-sdk .ot-ven-dets .ot-ven-pur h4{border-top:1px solid #e9e9e9;border-bottom:1px solid #e9e9e9;padding-bottom:5px;margin-bottom:5px;font-weight:bold}#onetrust-pc-sdk #ot-host-lst .ot-sel-all{float:right;position:relative;margin-right:42px;top:10px}#onetrust-pc-sdk #ot-host-lst .ot-sel-all input[type=checkbox]{width:auto;height:auto}#onetrust-pc-sdk #ot-host-lst .ot-sel-all label{height:20px;width:20px;padding-left:0px}#onetrust-pc-sdk #ot-host-lst .ot-acc-txt{overflow:hidden;width:95%}#onetrust-pc-sdk .ot-host-hdr{position:relative;z-index:1;pointer-events:none;width:calc(100% - 125px);float:left}#onetrust-pc-sdk .ot-host-name,#onetrust-pc-sdk .ot-host-desc{display:inline-block;width:90%}#onetrust-pc-sdk .ot-host-name{pointer-events:none}#onetrust-pc-sdk .ot-host-hdr>a{text-decoration:underline;font-size:.82em;position:relative;z-index:2;float:left;margin-bottom:5px;pointer-events:initial}#onetrust-pc-sdk .ot-host-name+a{margin-top:5px}#onetrust-pc-sdk .ot-host-name,#onetrust-pc-sdk .ot-host-name a,#onetrust-pc-sdk .ot-host-desc,#onetrust-pc-sdk .ot-host-info{color:dimgray;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-host-name,#onetrust-pc-sdk .ot-host-name a{font-weight:bold;font-size:.82em;line-height:1.3}#onetrust-pc-sdk .ot-host-name a{font-size:1em}#onetrust-pc-sdk .ot-host-expand{margin-top:3px;margin-bottom:3px;clear:both;display:block;color:#3860be;font-size:.72em;font-weight:normal}#onetrust-pc-sdk .ot-host-expand *{font-size:inherit}#onetrust-pc-sdk .ot-host-desc,#onetrust-pc-sdk .ot-host-info{font-size:.688em;line-height:1.4;font-weight:normal}#onetrust-pc-sdk .ot-host-desc{margin-top:10px}#onetrust-pc-sdk .ot-host-opt{margin:0;font-size:inherit;display:inline-block;width:100%}#onetrust-pc-sdk .ot-host-opt li>div div{font-size:.8em;padding:5px 0}#onetrust-pc-sdk .ot-host-opt li>div div:nth-child(1){width:30%;float:left}#onetrust-pc-sdk .ot-host-opt li>div div:nth-child(2){width:70%;float:left;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-host-info{border:none;display:inline-block;width:calc(100% - 10px);padding:10px;margin-bottom:10px;background-color:#f8f8f8}#onetrust-pc-sdk .ot-host-info>div{overflow:auto}#onetrust-pc-sdk #no-results{text-align:center;margin-top:30px}#onetrust-pc-sdk #no-results p{font-size:1em;color:#2e3644;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk #no-results p span{font-weight:bold}#onetrust-pc-sdk #ot-fltr-modal{width:100%;height:auto;display:none;-moz-transition:.2s ease;-o-transition:.2s ease;-webkit-transition:2s ease;transition:.2s ease;overflow:hidden;opacity:1;right:0}#onetrust-pc-sdk #ot-fltr-modal .ot-label-txt{display:inline-block;font-size:.85em;color:dimgray}#onetrust-pc-sdk #ot-fltr-cnt{z-index:2147483646;background-color:#fff;position:absolute;height:90%;max-height:300px;width:325px;left:210px;margin-top:10px;margin-bottom:20px;padding-right:10px;border-radius:3px;-webkit-box-shadow:0px 0px 12px 2px #c7c5c7;-moz-box-shadow:0px 0px 12px 2px #c7c5c7;box-shadow:0px 0px 12px 2px #c7c5c7}#onetrust-pc-sdk .ot-fltr-scrlcnt{overflow-y:auto;overflow-x:hidden;clear:both;max-height:calc(100% - 60px)}#onetrust-pc-sdk #ot-anchor{border:12px solid transparent;display:none;position:absolute;z-index:2147483647;right:55px;top:75px;transform:rotate(45deg);-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-webkit-transform:rotate(45deg);background-color:#fff;-webkit-box-shadow:-3px -3px 5px -2px #c7c5c7;-moz-box-shadow:-3px -3px 5px -2px #c7c5c7;box-shadow:-3px -3px 5px -2px #c7c5c7}#onetrust-pc-sdk .ot-fltr-btns{margin-left:15px}#onetrust-pc-sdk #filter-apply-handler{margin-right:15px}#onetrust-pc-sdk .ot-fltr-opt{margin-bottom:25px;margin-left:15px;width:75%;position:relative}#onetrust-pc-sdk .ot-fltr-opt p{display:inline-block;margin:0;font-size:.9em;color:#2e3644}#onetrust-pc-sdk .ot-chkbox label span{font-size:.85em;color:dimgray}#onetrust-pc-sdk .ot-chkbox input[type=checkbox]+label::after{content:none;color:#fff}#onetrust-pc-sdk .ot-chkbox input[type=checkbox]:checked+label::after{content:""}#onetrust-pc-sdk .ot-chkbox input[type=checkbox]:focus+label::before{outline-style:solid;outline-width:2px;outline-style:auto}#onetrust-pc-sdk #ot-selall-vencntr,#onetrust-pc-sdk #ot-selall-adtlvencntr,#onetrust-pc-sdk #ot-selall-hostcntr,#onetrust-pc-sdk #ot-selall-licntr,#onetrust-pc-sdk #ot-selall-gnvencntr{right:15px;position:relative;width:20px;height:20px;float:right}#onetrust-pc-sdk #ot-selall-vencntr label,#onetrust-pc-sdk #ot-selall-adtlvencntr label,#onetrust-pc-sdk #ot-selall-hostcntr label,#onetrust-pc-sdk #ot-selall-licntr label,#onetrust-pc-sdk #ot-selall-gnvencntr label{float:left;padding-left:0}#onetrust-pc-sdk #ot-ven-lst:first-child{border-top:1px solid #e2e2e2}#onetrust-pc-sdk ul{list-style:none;padding:0}#onetrust-pc-sdk ul li{position:relative;margin:0;padding:15px 15px 15px 10px;border-bottom:1px solid #e2e2e2}#onetrust-pc-sdk ul li h3{font-size:.75em;color:#656565;margin:0;display:inline-block;width:70%;height:auto;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk ul li p{margin:0;font-size:.7em}#onetrust-pc-sdk ul li input[type=checkbox]{position:absolute;cursor:pointer;width:100%;height:100%;opacity:0;margin:0;top:0;left:0}#onetrust-pc-sdk .ot-cat-item>button:focus,#onetrust-pc-sdk .ot-acc-cntr>button:focus,#onetrust-pc-sdk li>button:focus{outline:#000 solid 2px}#onetrust-pc-sdk .ot-cat-item>button,#onetrust-pc-sdk .ot-acc-cntr>button,#onetrust-pc-sdk li>button{position:absolute;cursor:pointer;width:100%;height:100%;margin:0;top:0;left:0;z-index:1;max-width:none;border:none}#onetrust-pc-sdk .ot-cat-item>button[aria-expanded=false]~.ot-acc-txt,#onetrust-pc-sdk .ot-acc-cntr>button[aria-expanded=false]~.ot-acc-txt,#onetrust-pc-sdk li>button[aria-expanded=false]~.ot-acc-txt{margin-top:0;max-height:0;opacity:0;overflow:hidden;width:100%;transition:.25s ease-out;display:none}#onetrust-pc-sdk .ot-cat-item>button[aria-expanded=true]~.ot-acc-txt,#onetrust-pc-sdk .ot-acc-cntr>button[aria-expanded=true]~.ot-acc-txt,#onetrust-pc-sdk li>button[aria-expanded=true]~.ot-acc-txt{transition:.1s ease-in;margin-top:10px;width:100%;overflow:auto;display:block}#onetrust-pc-sdk .ot-cat-item>button[aria-expanded=true]~.ot-acc-grpcntr,#onetrust-pc-sdk .ot-acc-cntr>button[aria-expanded=true]~.ot-acc-grpcntr,#onetrust-pc-sdk li>button[aria-expanded=true]~.ot-acc-grpcntr{width:auto;margin-top:0px;padding-bottom:10px}#onetrust-pc-sdk .ot-host-item>button:focus,#onetrust-pc-sdk .ot-ven-item>button:focus{outline:0;border:2px solid #000}#onetrust-pc-sdk .ot-hide-acc>button{pointer-events:none}#onetrust-pc-sdk .ot-hide-acc .ot-plus-minus>*,#onetrust-pc-sdk .ot-hide-acc .ot-arw-cntr>*{visibility:hidden}#onetrust-pc-sdk .ot-hide-acc .ot-acc-hdr{min-height:30px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt){padding-right:10px;width:calc(100% - 37px);margin-top:10px;max-height:calc(100% - 90px)}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) #ot-sel-blk{background-color:#f9f9fc;border:1px solid #e2e2e2;width:calc(100% - 2px);padding-bottom:5px;padding-top:5px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) .ot-sel-all{padding-right:34px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) .ot-sel-all-chkbox{width:auto}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) ul li{border:1px solid #e2e2e2;margin-bottom:10px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) .ot-acc-cntr>.ot-acc-hdr{padding:10px 0 10px 15px}#onetrust-pc-sdk.ot-addtl-vendors .ot-sel-all-chkbox{float:right}#onetrust-pc-sdk.ot-addtl-vendors .ot-plus-minus~.ot-sel-all-chkbox{right:34px}#onetrust-pc-sdk.ot-addtl-vendors #ot-ven-lst:first-child{border-top:none}#onetrust-pc-sdk .ot-acc-cntr{position:relative;border-left:1px solid #e2e2e2;border-right:1px solid #e2e2e2;border-bottom:1px solid #e2e2e2}#onetrust-pc-sdk .ot-acc-cntr input{z-index:1}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr{background-color:#f9f9fc;padding:5px 0 5px 15px;width:auto}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr .ot-plus-minus{vertical-align:middle;top:auto}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr .ot-arw-cntr{right:10px}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr input{z-index:2}#onetrust-pc-sdk .ot-acc-cntr>input[type=checkbox]:checked~.ot-acc-hdr{border-bottom:1px solid #e2e2e2}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-txt{padding-left:10px;padding-right:10px}#onetrust-pc-sdk .ot-acc-cntr button[aria-expanded=true]~.ot-acc-txt{width:auto}#onetrust-pc-sdk .ot-acc-cntr .ot-addtl-venbox{display:none}#onetrust-pc-sdk .ot-vlst-cntr{margin-bottom:0;width:100%}#onetrust-pc-sdk .ot-vensec-title{font-size:.813em;vertical-align:middle;display:inline-block}#onetrust-pc-sdk .category-vendors-list-handler,#onetrust-pc-sdk .category-vendors-list-handler+a{margin-left:0;margin-top:10px}#onetrust-pc-sdk #ot-selall-vencntr.line-through label::after,#onetrust-pc-sdk #ot-selall-adtlvencntr.line-through label::after,#onetrust-pc-sdk #ot-selall-licntr.line-through label::after,#onetrust-pc-sdk #ot-selall-hostcntr.line-through label::after,#onetrust-pc-sdk #ot-selall-gnvencntr.line-through label::after{height:auto;border-left:0;transform:none;-o-transform:none;-ms-transform:none;-webkit-transform:none;left:5px;top:9px}#onetrust-pc-sdk #ot-category-title{float:left;padding-bottom:10px;font-size:1em;width:100%}#onetrust-pc-sdk .ot-cat-grp{margin-top:10px}#onetrust-pc-sdk .ot-cat-item{line-height:1.1;margin-top:10px;display:inline-block;width:100%}#onetrust-pc-sdk .ot-btn-container{text-align:right}#onetrust-pc-sdk .ot-btn-container button{display:inline-block;font-size:.75em;letter-spacing:.08em;margin-top:19px}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon{position:absolute;top:10px;right:0;z-index:1;padding:0;background-color:transparent;border:none}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon:hover{opacity:.7}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon svg{display:block;height:10px;width:10px}#onetrust-pc-sdk #clear-filters-handler{margin-top:20px;margin-bottom:10px;float:right;max-width:200px;text-decoration:none;color:#3860be;font-size:.9em;font-weight:bold;background-color:transparent;border-color:transparent;padding:1px}#onetrust-pc-sdk #clear-filters-handler:hover{color:#2285f7}#onetrust-pc-sdk #clear-filters-handler:focus{outline:#000 solid 1px}#onetrust-pc-sdk .ot-accordion-layout.ot-cat-item{position:relative;border-radius:2px;margin:0;padding:0;border:1px solid #d8d8d8;border-top:none;width:calc(100% - 2px);float:left}#onetrust-pc-sdk .ot-accordion-layout.ot-cat-item:first-of-type{margin-top:10px;border-top:1px solid #d8d8d8}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpdesc{padding-left:20px;padding-right:20px;width:calc(100% - 40px);font-size:.812em;margin-bottom:10px;margin-top:15px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpdesc>ul{padding-top:10px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpdesc>ul li{padding-top:0;line-height:1.5;padding-bottom:10px}#onetrust-pc-sdk .ot-accordion-layout div+.ot-acc-grpdesc{margin-top:5px}#onetrust-pc-sdk .ot-accordion-layout .ot-vlst-cntr:first-child{margin-top:10px}#onetrust-pc-sdk .ot-accordion-layout .ot-vlst-cntr:last-child,#onetrust-pc-sdk .ot-accordion-layout .ot-hlst-cntr:last-child{margin-bottom:5px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-hdr{padding-top:11.5px;padding-bottom:11.5px;padding-left:20px;padding-right:20px;width:calc(100% - 40px);display:inline-block}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-txt{width:100%;padding:0px}#onetrust-pc-sdk .ot-accordion-layout .ot-subgrp-cntr{padding-left:20px;padding-right:15px;padding-bottom:0;width:calc(100% - 35px)}#onetrust-pc-sdk .ot-accordion-layout .ot-subgrp{padding-right:5px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpcntr{z-index:1;position:relative}#onetrust-pc-sdk .ot-accordion-layout .ot-cat-header+.ot-arw-cntr{position:absolute;top:50%;transform:translateY(-50%);right:20px;margin-top:-2px}#onetrust-pc-sdk .ot-accordion-layout .ot-cat-header+.ot-arw-cntr .ot-arw{width:15px;height:20px;margin-left:5px;color:dimgray}#onetrust-pc-sdk .ot-accordion-layout .ot-cat-header{float:none;color:#2e3644;margin:0;display:inline-block;height:auto;word-wrap:break-word;min-height:inherit}#onetrust-pc-sdk .ot-accordion-layout .ot-vlst-cntr,#onetrust-pc-sdk .ot-accordion-layout .ot-hlst-cntr{padding-left:20px;width:calc(100% - 20px);display:inline-block;margin-top:0px;padding-bottom:2px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-hdr{position:relative;min-height:25px}#onetrust-pc-sdk .ot-accordion-layout h4~.ot-tgl,#onetrust-pc-sdk .ot-accordion-layout h4~.ot-always-active{position:absolute;top:50%;transform:translateY(-50%);right:20px}#onetrust-pc-sdk .ot-accordion-layout h4~.ot-tgl+.ot-tgl{right:95px}#onetrust-pc-sdk .ot-accordion-layout .category-vendors-list-handler,#onetrust-pc-sdk .ot-accordion-layout .category-vendors-list-handler+a{margin-top:5px}#onetrust-pc-sdk .ot-enbl-chr h4~.ot-tgl,#onetrust-pc-sdk .ot-enbl-chr h4~.ot-always-active{right:45px}#onetrust-pc-sdk .ot-enbl-chr h4~.ot-tgl+.ot-tgl{right:120px}#onetrust-pc-sdk .ot-enbl-chr .ot-pli-hdr.ot-leg-border-color span:first-child{width:90px}#onetrust-pc-sdk .ot-enbl-chr li.ot-subgrp>h5+.ot-tgl-cntr{padding-right:25px}#onetrust-pc-sdk .ot-plus-minus{width:20px;height:20px;font-size:1.5em;position:relative;display:inline-block;margin-right:5px;top:3px}#onetrust-pc-sdk .ot-plus-minus span{position:absolute;background:#27455c;border-radius:1px}#onetrust-pc-sdk .ot-plus-minus span:first-of-type{top:25%;bottom:25%;width:10%;left:45%}#onetrust-pc-sdk .ot-plus-minus span:last-of-type{left:25%;right:25%;height:10%;top:45%}#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-arw,#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-plus-minus span:first-of-type,#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-plus-minus span:last-of-type{transform:rotate(90deg)}#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-plus-minus span:last-of-type{left:50%;right:50%}#onetrust-pc-sdk #ot-selall-vencntr label,#onetrust-pc-sdk #ot-selall-adtlvencntr label,#onetrust-pc-sdk #ot-selall-hostcntr label,#onetrust-pc-sdk #ot-selall-licntr label{position:relative;display:inline-block;width:20px;height:20px}#onetrust-pc-sdk .ot-host-item .ot-plus-minus,#onetrust-pc-sdk .ot-ven-item .ot-plus-minus{float:left;margin-right:8px;top:10px}#onetrust-pc-sdk .ot-ven-item ul{list-style:none inside;font-size:100%;margin:0}#onetrust-pc-sdk .ot-ven-item ul li{margin:0!important;padding:0;border:none!important}#onetrust-pc-sdk .ot-pli-hdr{color:#77808e;overflow:hidden;padding-top:7.5px;padding-bottom:7.5px;width:calc(100% - 2px);border-top-left-radius:3px;border-top-right-radius:3px}#onetrust-pc-sdk .ot-pli-hdr span:first-child{top:50%;transform:translateY(50%);max-width:90px}#onetrust-pc-sdk .ot-pli-hdr span:last-child{padding-right:10px;max-width:95px;text-align:center}#onetrust-pc-sdk .ot-li-title{float:right;font-size:.813em}#onetrust-pc-sdk .ot-pli-hdr.ot-leg-border-color{background-color:#f4f4f4;border:1px solid #d8d8d8}#onetrust-pc-sdk .ot-pli-hdr.ot-leg-border-color span:first-child{text-align:left;width:70px}#onetrust-pc-sdk li.ot-subgrp>h5,#onetrust-pc-sdk .ot-cat-header{width:calc(100% - 130px)}#onetrust-pc-sdk li.ot-subgrp>h5+.ot-tgl-cntr{padding-left:13px}#onetrust-pc-sdk .ot-acc-grpcntr .ot-acc-grpdesc{margin-bottom:5px}#onetrust-pc-sdk .ot-acc-grpcntr .ot-subgrp-cntr{border-top:1px solid #d8d8d8}#onetrust-pc-sdk .ot-acc-grpcntr .ot-vlst-cntr+.ot-subgrp-cntr{border-top:none}#onetrust-pc-sdk .ot-acc-hdr .ot-arw-cntr+.ot-tgl-cntr,#onetrust-pc-sdk .ot-acc-txt h4+.ot-tgl-cntr{padding-left:13px}#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item .ot-subgrp>h5,#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item .ot-cat-header{width:calc(100% - 145px)}#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item h5+.ot-tgl-cntr,#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item .ot-cat-header+.ot-tgl{padding-left:28px}#onetrust-pc-sdk .ot-sel-all-hdr,#onetrust-pc-sdk .ot-sel-all-chkbox{display:inline-block;width:100%;position:relative}#onetrust-pc-sdk .ot-sel-all-chkbox{z-index:1}#onetrust-pc-sdk .ot-sel-all{margin:0;position:relative;padding-right:23px;float:right}#onetrust-pc-sdk .ot-consent-hdr,#onetrust-pc-sdk .ot-li-hdr{float:right;font-size:.812em;line-height:normal;text-align:center;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-li-hdr{max-width:100px;padding-right:10px}#onetrust-pc-sdk .ot-consent-hdr{max-width:55px}#onetrust-pc-sdk #ot-selall-licntr{display:block;width:21px;height:auto;float:right;position:relative;right:80px}#onetrust-pc-sdk #ot-selall-licntr label{position:absolute}#onetrust-pc-sdk .ot-ven-ctgl{margin-left:66px}#onetrust-pc-sdk .ot-ven-litgl+.ot-arw-cntr{margin-left:81px}#onetrust-pc-sdk .ot-enbl-chr .ot-host-cnt .ot-tgl-cntr{width:auto}#onetrust-pc-sdk #ot-lst-cnt:not(.ot-host-cnt) .ot-tgl-cntr{width:auto;top:auto;height:20px}#onetrust-pc-sdk #ot-lst-cnt .ot-chkbox{position:relative;display:inline-block;width:20px;height:20px}#onetrust-pc-sdk #ot-lst-cnt .ot-chkbox label{position:absolute;padding:0;width:20px;height:20px}#onetrust-pc-sdk .ot-acc-grpdesc+.ot-leg-btn-container{padding-left:20px;padding-right:20px;width:calc(100% - 40px);margin-bottom:5px}#onetrust-pc-sdk .ot-subgrp .ot-leg-btn-container{margin-bottom:5px}#onetrust-pc-sdk #ot-ven-lst .ot-leg-btn-container{margin-top:10px}#onetrust-pc-sdk .ot-leg-btn-container{display:inline-block;width:100%;margin-bottom:10px}#onetrust-pc-sdk .ot-leg-btn-container button{height:auto;padding:6.5px 8px;margin-bottom:0;letter-spacing:0;font-size:.75em;line-height:normal}#onetrust-pc-sdk .ot-leg-btn-container svg{display:none;height:14px;width:14px;padding-right:5px;vertical-align:sub}#onetrust-pc-sdk .ot-active-leg-btn{cursor:default;pointer-events:none}#onetrust-pc-sdk .ot-active-leg-btn svg{display:inline-block}#onetrust-pc-sdk .ot-remove-objection-handler{text-decoration:underline;padding:0;font-size:.75em;font-weight:600;line-height:1;padding-left:10px}#onetrust-pc-sdk .ot-obj-leg-btn-handler span{font-weight:bold;text-align:center;font-size:inherit;line-height:1.5}#onetrust-pc-sdk.ot-close-btn-link #close-pc-btn-handler{border:none;height:auto;line-height:1.5;text-decoration:underline;font-size:.69em;background:none;right:15px;top:15px;width:auto;font-weight:normal}#onetrust-pc-sdk[dir=rtl] #ot-back-arw,#onetrust-pc-sdk[dir=rtl] input~.ot-acc-hdr .ot-arw{transform:rotate(180deg);-o-transform:rotate(180deg);-ms-transform:rotate(180deg);-webkit-transform:rotate(180deg)}#onetrust-pc-sdk[dir=rtl] input:checked~.ot-acc-hdr .ot-arw{transform:rotate(270deg);-o-transform:rotate(270deg);-ms-transform:rotate(270deg);-webkit-transform:rotate(270deg)}#onetrust-pc-sdk[dir=rtl] .ot-chkbox label::after{transform:rotate(45deg);-webkit-transform:rotate(45deg);-o-transform:rotate(45deg);-ms-transform:rotate(45deg);border-left:0;border-right:3px solid}#onetrust-pc-sdk[dir=rtl] .ot-search-cntr>svg{right:0}@media only screen and (max-width:600px){#onetrust-pc-sdk.otPcCenter{left:0;min-width:100%;height:100%;top:0;border-radius:0}#onetrust-pc-sdk #ot-pc-content,#onetrust-pc-sdk.ot-ftr-stacked .ot-btn-container{margin:1px 3px 0 10px;padding-right:10px;width:calc(100% - 23px)}#onetrust-pc-sdk .ot-btn-container button{max-width:none;letter-spacing:.01em}#onetrust-pc-sdk #close-pc-btn-handler{top:10px;right:17px}#onetrust-pc-sdk p{font-size:.7em}#onetrust-pc-sdk #ot-pc-hdr{margin:10px 10px 0 5px;width:calc(100% - 15px)}#onetrust-pc-sdk .vendor-search-handler{font-size:1em}#onetrust-pc-sdk #ot-back-arw{margin-left:12px}#onetrust-pc-sdk #ot-lst-cnt{margin:0;padding:0 5px 0 10px;min-width:95%}#onetrust-pc-sdk .switch+p{max-width:80%}#onetrust-pc-sdk .ot-ftr-stacked button{width:100%}#onetrust-pc-sdk #ot-fltr-cnt{max-width:320px;width:90%;border-top-right-radius:0;border-bottom-right-radius:0;margin:0;margin-left:15px;left:auto;right:40px;top:85px}#onetrust-pc-sdk .ot-fltr-opt{margin-left:25px;margin-bottom:10px}#onetrust-pc-sdk .ot-pc-refuse-all-handler{margin-bottom:0}#onetrust-pc-sdk #ot-fltr-cnt{right:40px}}@media only screen and (max-width:476px){#onetrust-pc-sdk .ot-fltr-cntr,#onetrust-pc-sdk #ot-fltr-cnt{right:10px}#onetrust-pc-sdk #ot-anchor{right:25px}#onetrust-pc-sdk button{width:100%}#onetrust-pc-sdk:not(.ot-addtl-vendors) #ot-pc-lst:not(.ot-enbl-chr) .ot-sel-all{padding-right:9px}#onetrust-pc-sdk:not(.ot-addtl-vendors) #ot-pc-lst:not(.ot-enbl-chr) .ot-tgl-cntr{right:0}}@media only screen and (max-width:896px)and (max-height:425px)and (orientation:landscape){#onetrust-pc-sdk.otPcCenter{left:0;top:0;min-width:100%;height:100%;border-radius:0}#onetrust-pc-sdk #ot-anchor{left:initial;right:50px}#onetrust-pc-sdk #ot-lst-title{margin-top:12px}#onetrust-pc-sdk #ot-lst-title *{font-size:inherit}#onetrust-pc-sdk #ot-pc-hdr input{margin-right:0;padding-right:45px}#onetrust-pc-sdk .switch+p{max-width:85%}#onetrust-pc-sdk #ot-sel-blk{position:static}#onetrust-pc-sdk #ot-pc-lst{overflow:auto}#onetrust-pc-sdk #ot-lst-cnt{max-height:none;overflow:initial}#onetrust-pc-sdk #ot-lst-cnt.no-results{height:auto}#onetrust-pc-sdk input{font-size:1em!important}#onetrust-pc-sdk p{font-size:.6em}#onetrust-pc-sdk #ot-fltr-modal{width:100%;top:0}#onetrust-pc-sdk ul li p,#onetrust-pc-sdk .category-vendors-list-handler,#onetrust-pc-sdk .category-vendors-list-handler+a,#onetrust-pc-sdk .category-host-list-handler{font-size:.6em}#onetrust-pc-sdk.ot-shw-fltr #ot-anchor{display:none!important}#onetrust-pc-sdk.ot-shw-fltr #ot-pc-lst{height:100%!important;overflow:hidden;top:0px}#onetrust-pc-sdk.ot-shw-fltr #ot-fltr-cnt{margin:0;height:100%;max-height:none;padding:10px;top:0;width:calc(100% - 20px);position:absolute;right:0;left:0;max-width:none}#onetrust-pc-sdk.ot-shw-fltr .ot-fltr-scrlcnt{max-height:calc(100% - 65px)}}#onetrust-consent-sdk #onetrust-pc-sdk,#onetrust-consent-sdk #ot-search-cntr,#onetrust-consent-sdk #onetrust-pc-sdk .ot-switch.ot-toggle,#onetrust-consent-sdk #onetrust-pc-sdk ot-grp-hdr1 .checkbox,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-title:after,#onetrust-consent-sdk #onetrust-pc-sdk #ot-sel-blk,#onetrust-consent-sdk #onetrust-pc-sdk #ot-fltr-cnt,#onetrust-consent-sdk #onetrust-pc-sdk #ot-anchor{background-color:#FFFFFF}#onetrust-consent-sdk #onetrust-pc-sdk h3,#onetrust-consent-sdk #onetrust-pc-sdk h4,#onetrust-consent-sdk #onetrust-pc-sdk h5,#onetrust-consent-sdk #onetrust-pc-sdk h6,#onetrust-consent-sdk #onetrust-pc-sdk p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-ven-lst .ot-ven-opts p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-desc,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-title,#onetrust-consent-sdk #onetrust-pc-sdk .ot-li-title,#onetrust-consent-sdk #onetrust-pc-sdk .ot-sel-all-hdr span,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-info,#onetrust-consent-sdk #onetrust-pc-sdk #ot-fltr-modal #modal-header,#onetrust-consent-sdk #onetrust-pc-sdk .ot-checkbox label span,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst #ot-sel-blk p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst #ot-lst-title h3,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst .back-btn-handler p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst .ot-ven-name,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst #ot-ven-lst .consent-category,#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-inactive-leg-btn,#onetrust-consent-sdk #onetrust-pc-sdk .ot-label-status,#onetrust-consent-sdk #onetrust-pc-sdk .ot-chkbox label span,#onetrust-consent-sdk #onetrust-pc-sdk #clear-filters-handler{color:#696969}#onetrust-consent-sdk #onetrust-pc-sdk .privacy-notice-link,#onetrust-consent-sdk #onetrust-pc-sdk .category-vendors-list-handler,#onetrust-consent-sdk #onetrust-pc-sdk .category-vendors-list-handler+a,#onetrust-consent-sdk #onetrust-pc-sdk .category-host-list-handler,#onetrust-consent-sdk #onetrust-pc-sdk .ot-ven-link,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-name a,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-acc-hdr .ot-host-expand,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-info a{color:#3860BE}#onetrust-consent-sdk #onetrust-pc-sdk .category-vendors-list-handler:hover{opacity:.7}#onetrust-consent-sdk #onetrust-pc-sdk .ot-acc-grpcntr.ot-acc-txt,#onetrust-consent-sdk #onetrust-pc-sdk .ot-acc-txt .ot-subgrp-tgl .ot-switch.ot-toggle{background-color:#F8F8F8}#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-info,#onetrust-consent-sdk #onetrust-pc-sdk .ot-acc-txt .ot-ven-dets{background-color:#F8F8F8}#onetrust-consent-sdk #onetrust-pc-sdk button:not(#clear-filters-handler):not(.ot-close-icon):not(#filter-btn-handler):not(.ot-remove-objection-handler):not(.ot-obj-leg-btn-handler):not([aria-expanded]):not(.ot-link-btn),#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-active-leg-btn{background-color:#FDC301;border-color:#FDC301;color:#0e100e}#onetrust-consent-sdk #onetrust-pc-sdk .ot-active-menu{border-color:#FDC301}#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-remove-objection-handler{background-color:transparent;border:1px solid transparent}#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-inactive-leg-btn{background-color:#FFFFFF;color:#78808E;border-color:#78808E}#onetrust-consent-sdk #onetrust-pc-sdk .ot-tgl input:focus+.ot-switch,.ot-switch .ot-switch-nob,.ot-switch .ot-switch-nob:before,#onetrust-pc-sdk .ot-checkbox input[type="checkbox"]:focus+label::before,#onetrust-pc-sdk .ot-chkbox input[type="checkbox"]:focus+label::before{outline-color:#000000;outline-width:1px}#onetrust-pc-sdk .ot-host-item>button:focus,#onetrust-pc-sdk .ot-ven-item>button:focus{border:1px solid #000000}#onetrust-consent-sdk #onetrust-pc-sdk *:focus,#onetrust-consent-sdk #onetrust-pc-sdk .ot-vlst-cntr>a:focus{outline:1px solid #000000}#onetrust-consent-sdk{font-family:Arial,sans-serif}#onetrust-pc-sdk .ot-always-active{font-size:14px;font-weight:bold;color:#09a501}#onetrust-consent-sdk #onetrust-pc-sdk .active-group{border-color:#FFFFFF}#onetrust-pc-sdk .ot-button-group button{font-weight:bold}#onetrust-consent-sdk #onetrust-pc-sdk .ot-button-group .onetrust-close-btn-handler{background-color:#d8d8d8!important;border-color:#d8d8d8!important}#onetrust-consent-sdk h3{font-size:14px!important}#onetrust-pc-sdk .pc-header{padding:10px}#onetrust-pc-sdk .ot-toggle .checkbox input:checked+label:after{background:#6cc04a}#onetrust-pc-sdk #pc-policy-text a{color:inherit}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon{display:none}#onetrust-pc-sdk .group-description ul{font-size:inherit}#onetrust-pc-sdk .group-description ul li{list-style:none}.ot-sdk-cookie-policy{font-family:inherit;font-size:16px}.ot-sdk-cookie-policy.otRelFont{font-size:1rem}.ot-sdk-cookie-policy h3,.ot-sdk-cookie-policy h4,.ot-sdk-cookie-policy h6,.ot-sdk-cookie-policy p,.ot-sdk-cookie-policy li,.ot-sdk-cookie-policy a,.ot-sdk-cookie-policy th,.ot-sdk-cookie-policy #cookie-policy-description,.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group,.ot-sdk-cookie-policy #cookie-policy-title{color:dimgray}.ot-sdk-cookie-policy #cookie-policy-description{margin-bottom:1em}.ot-sdk-cookie-policy h4{font-size:1.2em}.ot-sdk-cookie-policy h6{font-size:1em;margin-top:2em}.ot-sdk-cookie-policy th{min-width:75px}.ot-sdk-cookie-policy a,.ot-sdk-cookie-policy a:hover{background:#fff}.ot-sdk-cookie-policy thead{background-color:#f6f6f4;font-weight:bold}.ot-sdk-cookie-policy .ot-mobile-border{display:none}.ot-sdk-cookie-policy section{margin-bottom:2em}.ot-sdk-cookie-policy table{border-collapse:inherit}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy{font-family:inherit;font-size:1rem}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h3,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h4,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h6,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy p,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy li,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-title{color:dimgray}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description{margin-bottom:1em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-subgroup{margin-left:1.5em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group-desc,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-table-header,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy span,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td{font-size:.9em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td span,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td a{font-size:inherit}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group{font-size:1em;margin-bottom:.6em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-title{margin-bottom:1.2em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy>section{margin-bottom:1em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th{min-width:75px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a:hover{background:#fff}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy thead{background-color:#f6f6f4;font-weight:bold}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-mobile-border{display:none}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy section{margin-bottom:2em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-subgroup ul li{list-style:disc;margin-left:1.5em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-subgroup ul li h4{display:inline-block}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table{border-collapse:inherit;margin:auto;border:1px solid #d7d7d7;border-radius:5px;border-spacing:initial;width:100%;overflow:hidden}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table th,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table td{border-bottom:1px solid #d7d7d7;border-right:1px solid #d7d7d7}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr:last-child td{border-bottom:0px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr th:last-child,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr td:last-child{border-right:0px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-host,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-cookies-type{width:25%}.ot-sdk-cookie-policy[dir=rtl]{text-align:left}#ot-sdk-cookie-policy h3{font-size:1.5em}@media only screen and (max-width:530px){.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) table,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) thead,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tbody,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) th,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr{display:block}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) thead tr{position:absolute;top:-9999px;left:-9999px}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr{margin:0 0 1em 0}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr:nth-child(odd),.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr:nth-child(odd) a{background:#f6f6f4}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td{border:none;border-bottom:1px solid #eee;position:relative;padding-left:50%}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td:before{position:absolute;height:100%;left:6px;width:40%;padding-right:10px}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) .ot-mobile-border{display:inline-block;background-color:#e4e4e4;position:absolute;height:100%;top:0;left:45%;width:2px}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td:before{content:attr(data-label);font-weight:bold}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) li{word-break:break-word;word-wrap:break-word}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table{overflow:hidden}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table td{border:none;border-bottom:1px solid #d7d7d7}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy thead,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy tbody,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy tr{display:block}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-host,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-cookies-type{width:auto}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy tr{margin:0 0 1em 0}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td:before{height:100%;width:40%;padding-right:10px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td:before{content:attr(data-label);font-weight:bold}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy li{word-break:break-word;word-wrap:break-word}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy thead tr{position:absolute;top:-9999px;left:-9999px;z-index:-9999}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr:last-child td{border-bottom:1px solid #d7d7d7;border-right:0px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr:last-child td:last-child{border-bottom:0px}}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h5,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h6,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy li,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy p,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy span,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-title{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table th{background-color:#F8F8F8}.ot-floating-button__front{background-image:url(resources/sample/11.png)}#ot-sdk-btn-floating.ot-floating-button{position:fixed;bottom:10px;opacity:0;width:50px;height:50px;line-height:15px;cursor:pointer;background-color:transparent;transform-style:preserve-3d;transition:all 300ms ease;perspective:1000px;z-index:2147483646;animation:otFloatingBtnIntro 800ms ease 0ms 1 forwards}#ot-sdk-btn-floating.ot-floating-button.ot-hide{display:none}#ot-sdk-btn-floating.ot-floating-button::before,#ot-sdk-btn-floating.ot-floating-button::after{text-transform:none;line-height:1;user-select:none;pointer-events:none;position:absolute;transform:scale(0);opacity:0;transition:all 300ms ease;display:block;height:auto}#ot-sdk-btn-floating.ot-floating-button::before{content:"";border:5px solid transparent;z-index:1001;top:50%;border-left-width:0;border-right-color:#333;right:calc(0em - 5px);transform:translate(10px,-50%)}#ot-sdk-btn-floating.ot-floating-button::after{content:attr(title);position:absolute;text-align:center;top:50%;left:calc(100% + 5px);transform:translate(10px,-50%);font-size:.75rem;min-width:3em;max-width:21em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:5px;border-radius:.3ch;box-shadow:0 1em 2em -0.5em rgba(0,0,0,.35);background:#333;color:#fff;z-index:2147483645}#ot-sdk-btn-floating.ot-floating-button:hover::before,#ot-sdk-btn-floating.ot-floating-button:hover::after{opacity:1}#ot-sdk-btn-floating.ot-floating-button:hover::before{transform:translate(0.5em,-50%) scale(1)}#ot-sdk-btn-floating.ot-floating-button:hover::after{transform:translate(0.5em,-50%) scale(1)}#ot-sdk-btn-floating.ot-floating-button.ot-pc-open .ot-floating-button__front{transform:rotateY(-180deg)}#ot-sdk-btn-floating.ot-floating-button.ot-pc-open .ot-floating-button__back{transform:rotateY(0)}#ot-sdk-btn-floating .ot-floating-button__front,#ot-sdk-btn-floating .ot-floating-button__back{position:absolute;width:100%;height:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#6aaae4;border-radius:10px;box-shadow:0 4px 8px 0 rgba(0,0,0,.2);transition:transform .6s;transform-style:preserve-3d}#ot-sdk-btn-floating .ot-floating-button__front{background-color:#6aaae4;transform:rotateY(0)}#ot-sdk-btn-floating .ot-floating-button__front.custom-persistent-icon{background-position:center center;background-repeat:no-repeat;background-size:100%;border-radius:100px}#ot-sdk-btn-floating .ot-floating-button__front svg{width:30px;height:37px}#ot-sdk-btn-floating .ot-floating-button__back{background-color:#69c;transform:rotateY(-180deg)}#ot-sdk-btn-floating .ot-floating-button__back.custom-persistent-icon{background-position:center center;background-repeat:no-repeat;background-size:100%;border-radius:100px}#ot-sdk-btn-floating .ot-floating-button__back svg{width:24px;height:24px}#ot-sdk-btn-floating.ot-floating-button button{background-color:transparent;border:0;width:100%;height:100%;cursor:pointer}@keyframes otFloatingBtnIntro{0%{opacity:0;left:-75px}100%{opacity:1;left:1%}}@keyframes otFloatingBtnImageIntro{0%{opacity:0;transform:scale(0) rotate(-270deg)}100%{opacity:100%;transform:scale(0.95) rotate(0deg)}}</style><link type="image/x-icon" rel="shortcut icon" href="resources/sample/12.gif"><link rel="canonical" href="https://www.asda.com/account?request_origin=asda&amp;redirect_uri=https%3A%2F%2Fwww.asda.com%2Fgood-living%2Ftag%2Frecipes"><meta http-equiv="content-security-policy" content="default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;"></head><body><div id="app"><main class="theme-ghs" style><div class="app"><header><div class="header-container"><a id="logo" href="https://www.asda.com/" target="_self" tabindex="1" rel="noreferrer"><img src="resources/sample/13.svg" alt="Asda.com homepage"></a><nav><button id="need-help" class="basic need-help" type="button" tabindex="2" aria-live="polite">Need help?</button><button id="back-shop" class="basic back-shop" type="button" tabindex="3" aria-live="polite">Home</button></nav></div></header><div class="co-account"><h1 class="co-account__title">Account settings</h1><h4 class="co-account__description">Select a section below to view and edit your details</h4><div class="panel "><button class="panel__header "><h3 class="panel__title">Personal details</h3><span class="panel__toggle">View</span></button></div><div class="panel "><button class="panel__header "><h3 class="panel__title">Sign in details</h3><span class="panel__toggle">View</span></button></div><div class="acc-block"><div class="panel address-phone-book "><button class="panel__header "><h3 class="panel__title">Address &amp; phone book</h3><span class="panel__toggle">View</span></button></div></div><div class="wallet-card"><div class="panel open"><button class="panel__header add-bottom-border"><h3 class="panel__title">Wallet</h3><span class="panel__toggle">Close</span></button><main class="panel__main"><h3 class="empty-header-name">Payment Cards</h3><div class="co-add-card"><div class="custom-icon input-box"><label for="card-number">Card number</label><div class="input-box__wrapper false"><input type="tel" class=" error" placeholder spellcheck="false" autocomplete="off" maxlength="255" id="card-number" tabindex="0" aria-label="Card number" value data-fathom="cc-number"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">Sorry, that's not a valid card number</div></div><div class="input-box"><label for="add-card-name">Name on card:</label><div class="input-box__wrapper false"><input type="text" class=" error" placeholder spellcheck="false" autocomplete="off" maxlength="255" id="add-card-name" tabindex="0" aria-label="Name on card:" value data-fathom="cc-name"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">We do not recognise that name</div></div><div class="input-box"><label for="add-card-expiry">Expiry date</label><div class="input-box__wrapper false"><input type="tel" class=" error" placeholder="MM/YY" spellcheck="false" autocomplete="off" maxlength="5" id="add-card-expiry" tabindex="0" aria-label="Expiry date" value data-fathom="cc-exp"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">Enter expiry date</div></div><h4 class="address-form-title">Search for your postcode or <button type="button" class="link-toggle">type out address in full</button></h4><div class="half input-box"><label for="account-postcode-finder">Your postcode*</label><div class="input-box__wrapper false"><input type="text" class="half-width error" placeholder spellcheck="false" autocomplete="off" maxlength="255" id="account-postcode-finder" tabindex="0" aria-label="Your postcode*" value data-fathom="zip"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">Enter your postcode</div></div><div class="half last input-box"><label for="account-postcode-finder-house">House number or name</label><div class="input-box__wrapper false"><input type="text" class="half-width " placeholder spellcheck="false" autocomplete="off" maxlength="255" id="account-postcode-finder-house" tabindex="0" aria-label="House number or name" value></div></div><button id class="secondary full find-button" type="button" tabindex="0" aria-live="polite">Find</button></div></main></div></div><div class="permission-centre"><div class="panel "><button class="panel__header "><h3 class="panel__title">Permissions</h3><span class="panel__toggle">View</span></button></div></div></div><footer><div class="footer-wrapper"><div class="footer-top"><a class="website-feedback-link" href="https://ghs-optin-issues.asda.com/" target="_blank" rel="noopener noreferrer"><img class="feedback-icon" src="resources/sample/15.svg" alt="feedback icon"><span>Website Feedback</span></a></div><div class="footer-main"><div class="footer-logo"><img src="resources/sample/16.svg" alt="Asda"></div><div class="footer-links"><ul><li>© ASDA 2022</li><li><a href="https://groceries.asda.com/terms-and-conditions" target="_blank" rel="noopener noreferrer">Terms &amp; Conditions</a></li><li><a href="https://www.asda.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Centre</a></li><li><a href="https://www.asda.com/help/company-details" target="_blank" rel="noopener noreferrer">Asda Company Details</a></li></ul></div></div></div></footer></div></main></div><noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-NHVQ6SB" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript><iframe style="display:none;visibility:hidden" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;> <html><head><meta charset=&quot;utf-8&quot;><title></title><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body style=&quot;background-color:transparent&quot;><iframe style=&quot;display:none&quot; sandbox=&quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&quot; srcdoc=&quot;<!DOCTYPE html PUBLIC &amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;quot; &amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;quot;> <html><head><meta charset=&amp;quot;utf-8&amp;quot;><title></title><meta http-equiv=&amp;quot;content-security-policy&amp;quot; content=&amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;quot;></head><body style=&amp;quot;background-color:transparent&amp;quot;><iframe style=&amp;quot;display:none&amp;quot; sandbox=&amp;quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&amp;quot; srcdoc=&amp;quot;<!DOCTYPE html PUBLIC &amp;amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;amp;quot; &amp;amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;amp;quot;> <html><head><meta charset=&amp;amp;quot;utf-8&amp;amp;quot;><title></title><meta http-equiv=&amp;amp;quot;content-security-policy&amp;amp;quot; content=&amp;amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;amp;quot;></head><body style=&amp;amp;quot;background-color:transparent&amp;amp;quot;></body></html>&amp;quot; width=&amp;quot;1&amp;quot; height=&amp;quot;1&amp;quot; frameborder=&amp;quot;0&amp;quot;></iframe></body></html>&quot; width=&quot;1&quot; height=&quot;1&quot; frameborder=&quot;0&quot;></iframe></body></html>" width="0" height="0"></iframe><iframe style="display:none;visibility:hidden" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;> <html><head><meta charset=&quot;utf-8&quot;><title></title><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body style=&quot;background-color:transparent&quot;><iframe style=&quot;display:none&quot; sandbox=&quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&quot; srcdoc=&quot;<!DOCTYPE html PUBLIC &amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;quot; &amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;quot;> <html><head><meta charset=&amp;quot;utf-8&amp;quot;><title></title><meta http-equiv=&amp;quot;content-security-policy&amp;quot; content=&amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;quot;></head><body style=&amp;quot;background-color:transparent&amp;quot;><iframe style=&amp;quot;display:none&amp;quot; sandbox=&amp;quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&amp;quot; srcdoc=&amp;quot;<!DOCTYPE html PUBLIC &amp;amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;amp;quot; &amp;amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;amp;quot;> <html><head><meta charset=&amp;amp;quot;utf-8&amp;amp;quot;><title></title><meta http-equiv=&amp;amp;quot;content-security-policy&amp;amp;quot; content=&amp;amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;amp;quot;></head><body style=&amp;amp;quot;background-color:transparent&amp;amp;quot;></body></html>&amp;quot; width=&amp;quot;1&amp;quot; height=&amp;quot;1&amp;quot; frameborder=&amp;quot;0&amp;quot;></iframe></body></html>&quot; width=&quot;1&quot; height=&quot;1&quot; frameborder=&quot;0&quot;></iframe></body></html>" width="0" height="0"></iframe><iframe style="display:none" name="__tcfapiLocator" title="CMP Locator" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<html><head><meta charset=&quot;utf-8&quot;><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body></body></html>"></iframe><div id="onetrust-consent-sdk" class="onetrust-consent-sdk-box" data-apply-ot-banner-popup="false"><div class="onetrust-pc-dark-filter ot-hide ot-fade-in"></div><div id="onetrust-pc-sdk" class="otPcCenter ot-hide ot-fade-in ot-sdk-not-webkit" aria-modal="true" role="alertdialog" aria-label="How we protect your privacy" lang="en"><!-- Close Button --><div class="ot-pc-header"><!-- Logo Tag --><div class="ot-pc-logo" role="img" aria-label="Company Logo" style="background-image:url(resources/sample/17.bin);background-position:left"></div></div><!-- Close Button --><div id="ot-pc-content" class="ot-pc-scrollbar"><h2 id="ot-pc-title">How we protect your privacy</h2><div id="ot-pc-desc">We process your data to deliver content or advertisements and measure the delivery of such content or advertisements to extract insights about our website. We share this information with our partners on the basis of consent. You may exercise your right to consent, based on a specific purpose below or at a partner level in the link under each purpose. These choices will be signaled to our vendors participating in the Transparency and Consent Framework.</div><button id="accept-recommended-btn-handler">Allow All</button><section class="ot-sdk-row ot-cat-grp"><h3 id="ot-category-title"> Manage Consent Preferences</h3><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-1" aria-labelledby="ot-header-id-1"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-1">Essential Cookies</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-1">Essential cookies are always on. These are technical cookies that are required for the operation of our sites. Without using these our sites can’t operate properly.</p></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-2" aria-labelledby="ot-header-id-2"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-2">Experience Cookies</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-2" id="ot-group-id-2" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="2" checked aria-labelledby="ot-header-id-2"> <label class="ot-switch" for="ot-group-id-2"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Experience Cookies</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-2">Right now only some of these can be turned off. We’re working on the rest. Experience cookies allow Asda to recognise and count visitors to our sites.
+This means we can check our online services are working and help us make improvements on our online services.
+Experience cookies also allow us to remember the choices you make, such as the items in your basket or your display preferences.</p></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="4"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-4" aria-labelledby="ot-header-id-4"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-4">Asda’s Advertising Cookies</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-4" id="ot-group-id-4" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="4" checked aria-labelledby="ot-header-id-4"> <label class="ot-switch" for="ot-group-id-4"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Asda’s Advertising Cookies</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-4">We may record your visits to our websites, the pages you have visited and the links you have followed.
+We use this information, along with other information about you as a customer, to help make our advertising relevant to your interests both on our sites and other sites you may visit.
+We also use these to limit the number of times that you see an ad and to inform us how effective a particular ad has been.
+</p></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="STACK42"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-STACK42" aria-labelledby="ot-header-id-STACK42"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-STACK42">Personalised ads and content, ad and content measurement, audience insights and product development</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-STACK42" id="ot-group-id-STACK42" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="STACK42" checked aria-labelledby="ot-header-id-STACK42"> <label class="ot-switch" for="ot-group-id-STACK42"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Personalised ads and content, ad and content measurement, audience insights and product development</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_2"><h5>Select basic ads</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_2" aria-checked="false" role="switch" data-optanongroupid="IABV2_2" class="cookie-subgroup-handler" aria-label="Select basic ads" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_2"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Ads can be shown to you based on the content you’re viewing, the app you’re using, your approximate location, or your device type.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_3"><h5>Create a personalised ads profile</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_3" aria-checked="false" role="switch" data-optanongroupid="IABV2_3" class="cookie-subgroup-handler" aria-label="Create a personalised ads profile" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_3"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">A profile can be built about you and your interests to show you personalised ads that are relevant to you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_4"><h5>Select personalised ads</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_4" aria-checked="false" role="switch" data-optanongroupid="IABV2_4" class="cookie-subgroup-handler" aria-label="Select personalised ads" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_4"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Personalised ads can be shown to you based on a profile about you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_5"><h5>Create a personalised content profile</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_5" aria-checked="false" role="switch" data-optanongroupid="IABV2_5" class="cookie-subgroup-handler" aria-label="Create a personalised content profile" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_5"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">A profile can be built about you and your interests to show you personalised content that is relevant to you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_6"><h5>Select personalised content</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_6" aria-checked="false" role="switch" data-optanongroupid="IABV2_6" class="cookie-subgroup-handler" aria-label="Select personalised content" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_6"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Personalised content can be shown to you based on a profile about you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_7"><h5>Measure ad performance</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_7" aria-checked="false" role="switch" data-optanongroupid="IABV2_7" class="cookie-subgroup-handler" aria-label="Measure ad performance" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_7"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">The performance and effectiveness of ads that you see or interact with can be measured.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_8"><h5>Measure content performance</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_8" aria-checked="false" role="switch" data-optanongroupid="IABV2_8" class="cookie-subgroup-handler" aria-label="Measure content performance" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_8"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">The performance and effectiveness of content that you see or interact with can be measured.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_9"><h5>Apply market research to generate audience insights</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_9" aria-checked="false" role="switch" data-optanongroupid="IABV2_9" class="cookie-subgroup-handler" aria-label="Apply market research to generate audience insights" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_9"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Market research can be used to learn more about the audiences who visit sites/apps and view ads.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_10"><h5>Develop and improve products</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_10" aria-checked="false" role="switch" data-optanongroupid="IABV2_10" class="cookie-subgroup-handler" aria-label="Develop and improve products" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_10"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Your data can be used to improve existing systems and software, and to develop new products</p></li></ul></div><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="STACK42">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IABV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IABV2_1" aria-labelledby="ot-header-id-IABV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IABV2_1">Store and/or access information on a device</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-IABV2_1" id="ot-group-id-IABV2_1" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="IABV2_1" checked aria-labelledby="ot-header-id-IABV2_1"> <label class="ot-switch" for="ot-group-id-IABV2_1"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Store and/or access information on a device</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IABV2_1">Cookies, device identifiers, or other information can be stored or accessed on your device for the purposes presented to you.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IABV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISFV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISFV2_1" aria-labelledby="ot-header-id-ISFV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISFV2_1">Use precise geolocation data</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-ISFV2_1" id="ot-group-id-ISFV2_1" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="ISFV2_1" checked aria-labelledby="ot-header-id-ISFV2_1"> <label class="ot-switch" for="ot-group-id-ISFV2_1"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Use precise geolocation data</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISFV2_1">Your precise geolocation data can be used in support of one or more purposes. This means your location can be accurate to within several meters.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISFV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISFV2_2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISFV2_2" aria-labelledby="ot-header-id-ISFV2_2"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISFV2_2">Actively scan device characteristics for identification</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-ISFV2_2" id="ot-group-id-ISFV2_2" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="ISFV2_2" checked aria-labelledby="ot-header-id-ISFV2_2"> <label class="ot-switch" for="ot-group-id-ISFV2_2"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Actively scan device characteristics for identification</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISFV2_2">Your device can be identified based on a scan of your device's unique combination of characteristics.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISFV2_2">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISPV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISPV2_1" aria-labelledby="ot-header-id-ISPV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISPV2_1">Ensure security, prevent fraud, and debug</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISPV2_1">Your data can be used to monitor for and prevent fraudulent activity, and ensure systems and processes work properly and securely.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISPV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISPV2_2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISPV2_2" aria-labelledby="ot-header-id-ISPV2_2"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISPV2_2">Technically deliver ads or content</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISPV2_2">Your device can receive and send information that allows you to see and interact with ads and content.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISPV2_2">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IFEV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IFEV2_1" aria-labelledby="ot-header-id-IFEV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IFEV2_1">Match and combine offline data sources</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IFEV2_1">Data from offline data sources can be combined with your online activity in support of one or more purposes</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IFEV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IFEV2_2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IFEV2_2" aria-labelledby="ot-header-id-IFEV2_2"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IFEV2_2">Link different devices</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IFEV2_2">Different devices can be determined as belonging to you or your household in support of one or more of purposes.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IFEV2_2">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IFEV2_3"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IFEV2_3" aria-labelledby="ot-header-id-IFEV2_3"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IFEV2_3">Receive and use automatically-sent device characteristics for identification</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IFEV2_3">Your device might be distinguished from other devices based on information it automatically sends, such as IP address or browser type.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IFEV2_3">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><!-- Groups sections starts --><!-- Group section ends --><!-- Accordion Group section starts --><!-- Accordion Group section ends --></section></div><section id="ot-pc-lst" class="ot-hide ot-hosts-ui ot-pc-scrollbar"><div id="ot-pc-hdr"><div id="ot-lst-title"><button class="ot-link-btn back-btn-handler" aria-label="Back"><svg id="ot-back-arw" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 444.531 444.531" xml:space="preserve"><title>Back Button</title><g><path fill="#656565" d="M213.13,222.409L351.88,83.653c7.05-7.043,10.567-15.657,10.567-25.841c0-10.183-3.518-18.793-10.567-25.835
+ l-21.409-21.416C323.432,3.521,314.817,0,304.637,0s-18.791,3.521-25.841,10.561L92.649,196.425
+ c-7.044,7.043-10.566,15.656-10.566,25.841s3.521,18.791,10.566,25.837l186.146,185.864c7.05,7.043,15.66,10.564,25.841,10.564
+ s18.795-3.521,25.834-10.564l21.409-21.412c7.05-7.039,10.567-15.604,10.567-25.697c0-10.085-3.518-18.746-10.567-25.978
+ L213.13,222.409z"></path></g></svg></button><h3>Performance Cookies</h3></div><div class="ot-lst-subhdr"><div class="ot-search-cntr"><p role="status" class="ot-scrn-rdr"></p><label for="vendor-search-handler" class="ot-scrn-rdr"></label> <input id="vendor-search-handler" type="text" name="vendor-search-handler" value> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 -30 110 110" aria-hidden="true"><title>Search Icon</title><path fill="#2e3644" d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23
+ s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92
+ c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17
+ s-17-7.626-17-17S14.61,6,23.984,6z"></path></svg></div><div class="ot-fltr-cntr"><button id="filter-btn-handler" aria-label="Filter" aria-haspopup="true"><svg role="presentation" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 402.577 402.577" xml:space="preserve"><title>Filter Icon</title><g><path fill="#fff" d="M400.858,11.427c-3.241-7.421-8.85-11.132-16.854-11.136H18.564c-7.993,0-13.61,3.715-16.846,11.136
+ c-3.234,7.801-1.903,14.467,3.999,19.985l140.757,140.753v138.755c0,4.955,1.809,9.232,5.424,12.854l73.085,73.083
+ c3.429,3.614,7.71,5.428,12.851,5.428c2.282,0,4.66-0.479,7.135-1.43c7.426-3.238,11.14-8.851,11.14-16.845V172.166L396.861,31.413
+ C402.765,25.895,404.093,19.231,400.858,11.427z"></path></g></svg></button></div><div id="ot-anchor"></div><section id="ot-fltr-modal"><div id="ot-fltr-cnt"><button id="clear-filters-handler">Clear</button><div class="ot-fltr-scrlcnt ot-pc-scrollbar"><div class="ot-fltr-opts"><div class="ot-fltr-opt"><div class="ot-chkbox"><input id="chkbox-id" type="checkbox" aria-checked="false" class="category-filter-handler"> <label for="chkbox-id"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div></div></div><div class="ot-fltr-btns"><button id="filter-apply-handler">Apply</button> <button id="filter-cancel-handler">Cancel</button></div></div></div></section></div></div><section id="ot-lst-cnt" class="ot-host-cnt ot-pc-scrollbar"><div id="ot-sel-blk"><div class="ot-sel-all"><div class="ot-sel-all-hdr"><span class="ot-consent-hdr">Consent</span> <span class="ot-li-hdr">Leg.Interest</span></div><div class="ot-sel-all-chkbox"><div class="ot-chkbox" id="ot-selall-hostcntr"><input id="select-all-hosts-groups-handler" type="checkbox" aria-checked="false"> <label for="select-all-hosts-groups-handler"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div><div class="ot-chkbox" id="ot-selall-vencntr"><input id="select-all-vendor-groups-handler" type="checkbox" aria-checked="false"> <label for="select-all-vendor-groups-handler"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div><div class="ot-chkbox" id="ot-selall-licntr"><input id="select-all-vendor-leg-handler" type="checkbox" aria-checked="false"> <label for="select-all-vendor-leg-handler"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div></div></div></div><div class="ot-sdk-row"><div class="ot-sdk-column"><ul id="ot-ven-lst"></ul></div></div></section></section><div class="ot-pc-footer"><div class="ot-btn-container"> <button class="save-preference-btn-handler onetrust-close-btn-handler">Confirm My Choices</button></div><!-- Footer logo --><div class="ot-pc-footer-logo"><a href="https://www.onetrust.com/products/cookie-consent/" target="_blank" rel="noopener noreferrer" style="background-image:url(resources/sample/18.svg)" aria-label="Powered by OneTrust Opens in a new Tab"></a></div></div><!-- Cookie subgroup container --><!-- Vendor list link --><!-- Cookie lost link --><!-- Toggle HTML element --><!-- Checkbox HTML --><!-- plus minus--><!-- Arrow SVG element --><!-- Accordion basic element --><span class="ot-scrn-rdr" aria-atomic="true" aria-live="polite"></span><iframe class="ot-text-resize" title="onetrust-text-resize" style="position:absolute;top:-50000px;width:100em" aria-hidden="true" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<html><head><meta charset=&quot;utf-8&quot;><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body></body></html>"></iframe></div><div id="ot-sdk-btn-floating" title="Manage Preferences" class="ot-floating-button"><div class="ot-floating-button__front custom-persistent-icon"><button type="button" class="ot-floating-button__open" aria-label="Open Preferences"></button></div><div class="ot-floating-button__back custom-persistent-icon"><button type="button" class="ot-floating-button__close"><!--?xml version="1.0" encoding="UTF-8"?--> <svg role="presentation" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Banner_02" class="ot-floating-button__svg-fill" transform="translate(-318.000000, -725.000000)" fill="#ffffff" fill-rule="nonzero"><g id="Group-2" transform="translate(305.000000, 712.000000)"><g id="icon/16px/white/close"><polygon id="Line1" points="13.3333333 14.9176256 35.0823744 36.6666667 36.6666667 35.0823744 14.9176256 13.3333333"></polygon><polygon id="Line2" transform="translate(25.000000, 25.000000) scale(-1, 1) translate(-25.000000, -25.000000) " points="13.3333333 14.9176256 35.0823744 36.6666667 36.6666667 35.0823744 14.9176256 13.3333333"></polygon></g></g></g></g></svg></button></div></div></div></body></html>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <box>
+ <html:iframe id="html-iframe"/>
+ </box>
+</window>
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 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input type="text" name="test" class="focusme">
+ </body>
+</html>
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<string>} 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<object>} 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: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email" />
+ <input type="text" id="country" autocomplete="country"/>
+ <input type="text" id="postal-code" autocomplete="postal-code" />
+ <input type="text" id="address-line1" autocomplete="address-line1" />
+ <div>
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </div>
+ </form>
+ </form>
+ </body>
+ </html>
+ `,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email" />
+ <input type="text" id="country" autocomplete="country" hidden />
+ <input type="text" id="postal-code" autocomplete="postal-code" style="display:none" />
+ <input type="text" id="address-line1" autocomplete="address-line1" style="opacity:0" />
+ <div style="visibility: hidden">
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </div>
+ </form>
+ </body>
+ </html>
+ `,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email" />
+ <input type="text" id="country" autocomplete="country" hidden />
+ <input type="text" id="postal-code" autocomplete="postal-code" style="display:none" />
+ <input type="text" id="address-line1" autocomplete="address-line1" style="opacity:0" />
+ <div style="visibility: hidden">
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </div>
+ </form>
+ </body>
+ </html>
+ `,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <p><label>country: <input type="text" id="country" name="country" /></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel" /></label></p>
+ <p><label><input type="text" id="housenumber" /></label></p>
+ </form>
+ </body>
+ </html>`,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <p><label>country: <input type="text" id="country" name="country" /></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel" /></label></p>
+ <p><label><input type="text" id="housenumber" /></label></p>
+ <p><label><input type="text" id="addrcomplement" /></label></p>
+ </form>
+ </body>
+ </html>`,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="street-address" autocomplete="street-address"/>
+ <input type="text" id="address-line2" autocomplete="address-line2"/>
+ <input type="text" id="email" autocomplete="email"/>
+ </form>
+ </body>
+ </html>`,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="street-address" autocomplete="street-address"/>
+ <input type="text" id="address-line3" autocomplete="address-line3"/>
+ <input type="text" id="email" autocomplete="email"/>
+ </form>
+ </body>
+ </html>`,
+ 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: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="street-address" autocomplete="street-address"/>
+ <input type="text" id="address-line1" autocomplete="address-line1"/>
+ <input type="text" id="email" autocomplete="email"/>
+ </form>
+ </body>
+ </html>`,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="email" autocomplete="email">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="email" autocomplete="email">
+
+ <input id="postal-code" autocomplete="postal-code">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `One billing section and one shipping section`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="postal-code" autocomplete="shipping postal-code">
+ <input id="country" autocomplete="shipping country">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="postal-code" autocomplete="shipping postal-code">
+ <input id="country" autocomplete="shipping country">
+ <input id="country" autocomplete="billing country">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="name" autocomplete="name">
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="postal-code" autocomplete="shipping postal-code">
+ <input id="country" autocomplete="shipping country">
+ <input id="name" autocomplete="name">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="name" autocomplete="name">
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="name" autocomplete="name">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ </body></html>
+ `,
+ 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: `
+ <html><body>
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ 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 <input> elements, and the following two <select>
+ // elements are the correct ones. BTW, they are both applied
+ // autocomplete attr.
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ { fieldName: "cc-number", reason: "fathom" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "cc-exp-month", reason: "regex-heuristic"},
+ { fieldName: "cc-exp-year", reason: "regex-heuristic"},
+ // {fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email",reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email",reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/HomeDepot/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js
new file mode 100644
index 0000000000..801dbf66be
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js
@@ -0,0 +1,28 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-type", reason: "regex-heuristic" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Lufthansa/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js
new file mode 100644
index 0000000000..a59fc966e5
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js
@@ -0,0 +1,31 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "index.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-name", reason: "fathom" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Lush/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js
new file mode 100644
index 0000000000..e02996b565
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js
@@ -0,0 +1,88 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_ShippingAddress.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Checkout_Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-type" }, // ac-off
+ { fieldName: "cc-number", reason: "fathom" }, // ac-off
+ { fieldName: "cc-exp-month" }, // ac-off
+ { fieldName: "cc-exp-year" }, // ac-off
+ // {fieldName: "cc-csc"}, // ac-off
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ // Sign in
+ { fieldName: "email", reason: "regex-heuristic"},
+ // {fieldName: "password"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ // Forgot password
+ { fieldName: "email", reason: "regex-heuristic"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic"},
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Macys/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js
new file mode 100644
index 0000000000..d914f02890
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js
@@ -0,0 +1,109 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingInfo.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "BillingInfo.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Login.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" }, // Email Address
+ { fieldName: "email", reason: "regex-heuristic" }, // Confirm Email Address
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/NewEgg/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js
new file mode 100644
index 0000000000..8c44104cb6
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js
@@ -0,0 +1,83 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingAddress.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "postal-code" },
+ { fieldName: "address-level2" }, // City & State
+ { fieldName: "address-level2" }, // City
+ { fieldName: "address-level1" }, // State
+ { fieldName: "tel-area-code" },
+ { fieldName: "tel-local-prefix" },
+ { fieldName: "tel-local-suffix" },
+ { fieldName: "tel-extension" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ invalid: true, // because non of them is identified by fathom
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ { fieldName: "cc-number" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "postal-code" },
+ { fieldName: "address-level2" }, // City & State
+ { fieldName: "address-level2" }, // City
+ { fieldName: "address-level1" }, // state
+ { fieldName: "tel-area-code" },
+ { fieldName: "tel-local-prefix" },
+ { fieldName: "tel-local-suffix" },
+ { fieldName: "tel-extension" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/OfficeDepot/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js
new file mode 100644
index 0000000000..bc0504e726
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js
@@ -0,0 +1,96 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "YourInformation.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "tel", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ // { fieldName: "bday-month"}, // select
+ // { fieldName: "bday-day"}, // select
+ // { fieldName: "bday-year"},
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-type", reason: "regex-heuristic" },
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp", reason: "regex-heuristic" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "cc-number", reason: "regex-heuristic" }, // txtQvcGiftCardNumber
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "PaymentMethod.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "tel", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ // { fieldName: "bday-month"}, // select
+ // { fieldName: "bday-day"}, // select
+ // { fieldName: "bday-year"}, // select
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-type", reason: "regex-heuristic" }, // ac-off
+ { fieldName: "cc-number" }, // ac-off
+ { fieldName: "cc-exp", reason: "regex-heuristic" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "cc-number", reason: "regex-heuristic" }, // txtQvcGiftCardNumbe, ac-off
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ // Sign in
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/QVC/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js
new file mode 100644
index 0000000000..4c668ddad1
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js
@@ -0,0 +1,81 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingAddress.html",
+ expectedResult: [
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "email" },
+ ]
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // check-out, ac-off
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "tel-extension" },
+ { fieldName: "email" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // ac-off
+ // { fieldName: "email"},
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "tel-extension" },
+ // { fieldName: "new-password"},
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // ac-off
+ { fieldName: "email" },
+ ]
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // ac-off
+ { fieldName: "email" },
+ ]
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Sears/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js
new file mode 100644
index 0000000000..4b03fe2338
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js
@@ -0,0 +1,78 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Basic.html",
+ expectedResult: [
+ {
+ // ac-off
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "email" },
+ { fieldName: "tel" },
+ { fieldName: "tel" }, // Extension
+ { fieldName: "organization" },
+ ]
+ },
+ ],
+ },
+ {
+ fixturePath: "Basic_ac_on.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "email" },
+ { fieldName: "tel" },
+ { fieldName: "tel" }, // Extension
+ { fieldName: "organization" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "PaymentBilling.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp" },
+ // {fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "PaymentBilling_ac_on.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Staples/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js
new file mode 100644
index 0000000000..f2896bd9ac
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js
@@ -0,0 +1,93 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "postal-code", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ // { fieldName: "password"}, // ac-off
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "email" }, // ac-off
+ // { fieldName: "password"},
+ // { fieldName: "password"}, // ac-off
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ section: "section-payment",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "tel" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ section: "section-payment",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Shipping.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "postal-code", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "tel" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Walmart/"
+);
diff --git a/browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html b/browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html
new file mode 100644
index 0000000000..73b5e51cdb
--- /dev/null
+++ b/browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Address Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Address Demo Page</h1>
+ <form id="form">
+ <p><label>givenname: <input type="text" id="given-name" name="given-name" autocomplete="given-name" /></label></p>
+ <p><label>familyname: <input type="text" id="family-name" name="family-name" autocomplete="family-name" /></label></p>
+ <p><label>organization: <input type="text" id="organization" name="organization" autocomplete="organization" /></label></p>
+ <p><label>streetAddress: <input type="text" id="street-address" name="street-address" autocomplete="street-address" /></label></p>
+ <p><label>addressLevel2: <input type="text" id="address-level2" name="address-level2" autocomplete="address-level2" /></label></p>
+ <p><label>addressLevel1: <input type="text" id="address-level1" name="address-level1" autocomplete="address-level1" /></label></p>
+ <p><label>postalCode: <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" /></label></p>
+ <p><label>country: <input type="text" id="country" name="country" autocomplete="country" /></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel" autocomplete="tel" /></label></p>
+ <p><label>email: <input type="text" id="email" name="email" autocomplete="email" /></label></p>
+ <p>
+ <input type="submit"/>
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Demo Page</h1>
+ <form id="form">
+ <p><label>organization: <input type="text" id="organization" name="organization" autocomplete="organization" /></label></p>
+ <p><label>streetAddress: <input type="text" id="street-address" name="street-address" autocomplete="street-address" /></label></p>
+ <p><label>addressLevel2: <input type="text" id="address-level2" name="address-level2" autocomplete="address-level2" /></label></p>
+ <p><label>addressLevel1: <input type="text" id="address-level1" name="address-level1" autocomplete="address-level1" /></label></p>
+ <p><label>postalCode: <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" /></label></p>
+ <p><label>country: <input type="text" id="country" name="country" autocomplete="country" /></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel" autocomplete="tel" /></label></p>
+ <p><label>email: <input type="text" id="email" name="email" autocomplete="email" /></label></p>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+ <form id="formB">
+ <p><label>Organization: <input type="text" /></label></p>
+ <p><label><input type="text" id="B_address-line1" /></label></p>
+ <p><label><input type="text" name="address-line2" /></label></p>
+ <p><label><input type="text" id="B_address-line3" name="address-line3" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" /></label></p>
+ <p><label>State: <select id="B_address-level1" ></select></label></p>
+ <p><input type="text" id="B_postal-code" name="postal-code" /></p>
+ <p><label>Country: <select multiple id="B_country" name="country" ></select></label></p>
+ <p><label>Telephone: <input id="B_tel" name="tel" /></label></p>
+ <p><label>Email: <input type="text" id="B_email" name="email" /></label></p>
+ <hr>
+ <p><label>cc-number <input type="text" id="B_cc-number" autocomplete="cc-number" /></label></p>
+ <p><label>cc-name <input type="text" id="B_cc-name" autocomplete="cc-name" /></label></p>
+ <p><label>cc-exp-month <input type="text" id="B_cc-exp-month" autocomplete="cc-exp-month" /></label></p>
+ <p><label>cc-exp-year <input type="text" id="B_cc-exp-year" autocomplete="cc-exp-year" /></label></p>
+ <hr>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+ <form id="formC">
+ <p><label><input type="text" name="someprefixAddrLine1" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" /></label></p>
+ <p><label><input type="text" name="someprefixAddrLine2" /></label></p>
+ <p><label>Organization: <input type="text" name="organization" /></label></p>
+ <p><label><input type="text" name="someprefixAddrLine3" /></label></p>
+ </form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Credit Card Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Credit Card Demo Page</h1>
+ <form id="form">
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ <p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
+ <option></option>
+ <option value="discover">Discover</option>
+ <option value="jcb">JCB</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">MasterCard</option>
+ <option value="gringotts">Unknown card network</option>
+ </select></label></p>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Credit Card Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Credit Card Demo Page</h1>
+ <form id="form">
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration string: <input id="cc-exp" autocomplete="cc-exp"></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ <p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
+ <option></option>
+ <option value="discover">Discover</option>
+ <option value="jcb">JCB</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">MasterCard</option>
+ <option value="gringotts">Unknown card network</option>
+ </select></label></p>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Credit Card With Remote IFrame Demo Page</title>
+</head>
+<body>
+ <iframe src="https://test1.example.com:443/browser/browser/extensions/formautofill/test/browser/creditCard/autocomplete_creditcard_basic.html" width="400" height="400">
+ </iframe>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill With Remote IFrame Demo Page</title>
+</head>
+<body>
+ <iframe id="unused" src="data:text/html,<body>Just here to ensure code doesn't always pick the first child iframe.</body>"></iframe>
+ <iframe src="https://test1.example.com:443/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html" width="400" height="400">
+ </iframe>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Demo Page with autocomplete set to off on form elements</title>
+</head>
+<body>
+ <h1>Form Autofill Demo Page with autocomplete set to off on form elements</h1>
+ <form id="form" autocomplete="off">
+ <p><label>organization: <input type="text" id="organization" name="organization" autocomplete="organization" /></label></p>
+ <p><label>streetAddress: <input type="text" id="street-address" name="street-address" autocomplete="street-address" /></label></p>
+ <p><label>addressLevel2: <input type="text" id="address-level2" name="address-level2" autocomplete="address-level2" /></label></p>
+ <p><label>addressLevel1: <input type="text" id="address-level1" name="address-level1" autocomplete="address-level1" /></label></p>
+ <p><label>postalCode: <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" /></label></p>
+ <p><label>country: <input type="text" id="country" name="country" autocomplete="country" /></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel" autocomplete="tel" /></label></p>
+ <p><label>email: <input type="text" id="email" name="email" autocomplete="email" /></label></p>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+ <form id="formB" autocomplete="off">
+ <p><label>Organization: <input type="text" /></label></p>
+ <p><label><input type="text" id="B_address-line1" /></label></p>
+ <p><label><input type="text" name="address-line2" /></label></p>
+ <p><label><input type="text" id="B_address-line3" name="address-line3" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" /></label></p>
+ <p><label>State: <select id="B_address-level1" ></select></label></p>
+ <p><input type="text" id="B_postal-code" name="postal-code" /></p>
+ <p><label>Country: <select multiple id="B_country" name="country" ></select></label></p>
+ <p><label>Telephone: <input id="B_tel" name="tel" /></label></p>
+ <p><label>Email: <input type="text" id="B_email" name="email" /></label></p>
+ <hr>
+ <p><label>cc-number <input type="text" id="B_cc-number" autocomplete="cc-number" /></label></p>
+ <p><label>cc-name <input type="text" id="B_cc-name" autocomplete="cc-name" /></label></p>
+ <p><label>cc-exp-month <input type="text" id="B_cc-exp-month" autocomplete="cc-exp-month" /></label></p>
+ <p><label>cc-exp-year <input type="text" id="B_cc-exp-year" autocomplete="cc-exp-year" /></label></p>
+ <hr>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+ <form id="formC" autocomplete="off">
+ <p><label><input type="text" name="someprefixAddrLine1" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" /></label></p>
+ <p><label><input type="text" name="someprefixAddrLine2" /></label></p>
+ <p><label>Organization: <input type="text" name="organization" /></label></p>
+ <p><label><input type="text" name="someprefixAddrLine3" /></label></p>
+ </form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Demo Page with autocomplete set to off on inputs within form elements</title>
+</head>
+<body>
+ <h1>Form Autofill Demo Page with autocomplete set to off on inputs within form elements</h1>
+ <form id="form">
+ <p><label>organization: <input type="text" id="organization" name="organization" autocomplete="off" /></label></p>
+ <p><label>streetAddress: <input type="text" id="street-address" name="street-address" autocomplete="off" /></label></p>
+ <p><label>addressLevel2: <input type="text" id="address-level2" name="address-level2" autocomplete="off" /></label></p>
+ <p><label>addressLevel1: <input type="text" id="address-level1" name="address-level1" autocomplete="off" /></label></p>
+ <p><label>postalCode: <input type="text" id="postal-code" name="postal-code" autocomplete="off" /></label></p>
+ <p><label>country: <input type="text" id="country" name="country" autocomplete="off" /></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel" autocomplete="off" /></label></p>
+ <p><label>email: <input type="text" id="email" name="email" autocomplete="off" /></label></p>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+ <form id="formB">
+ <p><label>Organization: <input type="text" autocomplete="off" /></label></p>
+ <p><label><input type="text" id="B_address-line1" autocomplete="off" /></label></p>
+ <p><label><input type="text" name="address-line2" autocomplete="off" /></label></p>
+ <p><label><input type="text" id="B_address-line3" name="address-line3" autocomplete="off" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" autocomplete="off" /></label></p>
+ <p><label>State: <select id="B_address-level1" autocomplete="off" ></select></label></p>
+ <p><input type="text" id="B_postal-code" name="postal-code" autocomplete="off" /></p>
+ <p><label>Country: <select multiple id="B_country" name="country" autocomplete="off" ></select></label></p>
+ <p><label>Telephone: <input id="B_tel" name="tel" autocomplete="off" /></label></p>
+ <p><label>Email: <input type="text" id="B_email" name="email" autocomplete="off" /></label></p>
+ <hr>
+ <p><label>cc-number <input type="text" id="B_cc-number" autocomplete="off" /></label></p>
+ <p><label>cc-name <input type="text" id="B_cc-name" autocomplete="off" /></label></p>
+ <p><label>cc-exp-month <input type="text" id="B_cc-exp-month" autocomplete="off" /></label></p>
+ <p><label>cc-exp-year <input type="text" id="B_cc-exp-year" autocomplete="off" /></label></p>
+ <hr>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+ <form id="formC">
+ <p><label><input type="text" name="someprefixAddrLine1" autocomplete="off" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" autocomplete="off" /></label></p>
+ <p><label><input type="text" name="someprefixAddrLine2" autocomplete="off" /></label></p>
+ <p><label>Organization: <input type="text" name="organization" autocomplete="off" /></label></p>
+ <p><label><input type="text" name="someprefixAddrLine3" autocomplete="off" /></label></p>
+ </form>
+
+ <form id="formD">
+ <!--
+ Ensure heuristics can correctly identify fields when there are
+ autocomplete="off" fields as well as missing autocomplete attributes
+ -->
+ <p><label>Organization: <input type="text" autocomplete="organization" /></label></p>
+ <p><label><input type="text" id="B_address-line1" autocomplete="off" /></label></p>
+ <p><label><input type="text" name="address-line2" /></label></p>
+ <p><label><input type="text" id="B_address-line3" name="address-line3" autocomplete="off" /></label></p>
+ <p><label>City: <input type="text" name="address-level2" /></label></p>
+ <p><label>State: <select id="B_address-level1" autocomplete="address-level1" ></select></label></p>
+ <p><input type="text" id="B_postal-code" name="postal-code" /></p>
+ <p><label>Country: <select multiple id="B_country" name="country" autocomplete="off" ></select></label></p>
+ <p><label>Telephone: <input id="B_tel" name="tel" autocomplete="tel" /></label></p>
+ <p><label>Email: <input type="text" id="B_email" name="email" autocomplete="off" /></label></p>
+ <hr>
+ <p><label>cc-number <input type="text" id="B_cc-number" autocomplete="off" /></label></p>
+ <p><label>cc-name <input type="text" id="B_cc-name" autocomplete="cc-name" /></label></p>
+ <p><label>cc-exp-month <input type="text" id="B_cc-exp-month" /></label></p>
+ <p><label>cc-exp-year <input type="text" id="B_cc-exp-year" /></label></p>
+ <hr>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Demo Page for Simplified Form Case</title>
+</head>
+<body>
+ <h1>Form Autofill Demo Page for Simplified Form Case</h1>
+
+ <form id="simple">
+ <p><label>Organization: <input type="text" /></label></p>
+ <p><label>Telephone: <input id="simple_tel" name="tel" /></label></p>
+ <p><label>Email: <input type="text" id="simple_email" name="email" /></label></p>
+ <p><input type="submit" /></p>
+ <p><button type="reset">Reset</button></p>
+ </form>
+
+</body>
+</html>
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..4d8cb4a101
--- /dev/null
+++ b/browser/extensions/formautofill/test/fixtures/heuristics_cc_exp.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Heuristics cc-exp field test page</title>
+</head>
+<body>
+ <h1>Heuristics cc-exp field test page</h1>
+
+ <form id="form1">
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+
+ <form id="form2">
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration Date: <input autocomplete="cc-exp"></label></p>
+ </form>
+
+ <form id="form3">
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration Date: <input type="text"></label></p>
+ </form>
+
+ <form id="form4">
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p>
+ <label>Exp:
+ <select>
+ <option value="1"></option>
+ <option value="2"></option>
+ <option value="3"></option>
+ <option value="4"></option>
+ <option value="5"></option>
+ <option value="6"></option>
+ <option value="7"></option>
+ <option value="8"></option>
+ <option value="9"></option>
+ <option value="10"></option>
+ <option value="11"></option>
+ <option value="12"></option>
+ </select>
+ </label>
+ </p>
+ <p>
+ <label>Exp:
+ <select>
+ <option value="2016"></option>
+ <option value="2017"></option>
+ <option value="2018"></option>
+ <option value="2019"></option>
+ <option value="2020"></option>
+ <option value="2021"></option>
+ <option value="2022"></option>
+ <option value="2023"></option>
+ <option value="2024"></option>
+ <option value="2025"></option>
+ <option value="2026"></option>
+ </select>
+ </label>
+ </p>
+ </form>
+
+ <form id="form5">
+ <input class="expire-date" maxlength="2" id="expiry-month" placeholder="MM" name="expireMonth" type="text">
+ <input id="expiry-year" class="expire-date" placeholder="YY" maxlength="2" name="expireYear" type="text">
+ <input maxlength="3" name="cvc" type="text">
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Heuristics de-DE fields test page</title>
+</head>
+<body>
+ <h1>Heuristics de-DE fields test page</h1>
+ <form autocomplete="off">
+ <div>
+ <div>Karteninhaber</div>
+ <input id="creditCardHolder" name="creditCardHolder" maxlength="30" type="text">
+ </div>
+ <div>
+ <div>Kartentyp</div>
+ <select id="CCBrand" name="CCBrand">
+ <option>
+ </option>
+ <option>AMEX</option>
+ <option>VISA</option>
+ <option>MasterCard</option>
+ <option>Maestro</option>
+ </select>
+ </div>
+ <div>
+ <div>Kartennummer</div>
+ <input id="CCNr" name="CCNr" maxlength="19" type="text">
+ </div>
+ <div>
+ <div>gültig bis</div>
+ <select id="KKMonth" name="KKMonth">
+ <option value="MM">MM</option>
+ <option value="01">01</option>
+ <option value="02">02</option>
+ <option value="03">03</option>
+ <option value="04">04</option>
+ <option value="05">05</option>
+ <option value="06">06</option>
+ <option value="07">07</option>
+ <option value="08">08</option>
+ <option value="09">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ <select id="KKYear" name="KKYear">
+ <option value="YYYY">JJJJ</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ </select>
+ </div>
+ <div>
+ <div>Prüfnummer</div>
+ <input name="cccvc" id="CVV" maxlength="4" size="5" type="password">
+ </div>
+ </form>
+ <form autocomplete="off">
+ <div>
+ <label for="test-input">Name auf der Karte</label>
+ <input id="test-input" maxlength="30" type="text" name="ppw-accountHolderName">
+ </div>
+ <div>
+ <label for="DE_select">Kartenmarke</label>
+ <select id="DE_select" name="KKName">
+ <option>
+ </option>
+ <option>AMEX</option>
+ <option>VISA</option>
+ <option>MasterCard</option>
+ <option>Maestro</option>
+ </select>
+ </div>
+ <div>
+ <label for="test-input3">Kartennummer</label>
+ <input id="test-input3" maxlength="19" type="text">
+ </div>
+ <div>
+ <div>gültig bis</div>
+ <select id="KKMonth" name="KKMonth">
+ <option value="MM">MM</option>
+ <option value="01">01</option>
+ <option value="02">02</option>
+ <option value="03">03</option>
+ <option value="04">04</option>
+ <option value="05">05</option>
+ <option value="06">06</option>
+ <option value="07">07</option>
+ <option value="08">08</option>
+ <option value="09">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ <select id="KKYear" name="KKYear">
+ <option value="YYYY">JJJJ</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ </select>
+ </div>
+ <div>
+ <div>Prüfnummer</div>
+ <input name="cccvc" id="CVV" maxlength="4" size="5" type="password">
+ </div>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Heuristics fr-FR fields test page</title>
+</head>
+
+<body>
+ <h1>Heuristics fr-FR fields test page</h1>
+ <form name="cardForm">
+ <div>
+ <input id="cardNumber" autocomplete="off" name="cardNumber" maxlength="19" placeholder="**** **** **** ****"
+ type="tel" value="">
+ <label for="cardNumber">Numéro de carte *</label>
+ </div>
+ <div>
+ <input id="expiry" autocomplete="off" name="expiry" maxlength="5" placeholder="MM/AA" step="1" type="tel"
+ value="">
+ <label for="expiry">Date d'expiration *</label>
+ </div>
+ <div>
+ <input id="cvc" autocomplete="off" name="cvc" maxlength="3" placeholder="***" step="1" type="tel" value="">
+ <label for="cvc">Cryptogramme *</label>
+ </div>
+ <div>
+ <input id="name" autocomplete="off" name="name" placeholder="Nom Prénom" step="1" type="text" value="DOE JOHN">
+ <label for="name">Titulaire *</label>
+ </div>
+ </form>
+</body>
+
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Demo Page</h1>
+ <form>
+ <label>Name: <input id="name" autocomplete="name"></label><br/>
+ <label>Organization: <input id="organization" autocomplete="organization"></label><br/>
+
+ <br/>
+ <label>Street Address: <input id="street-address-a" autocomplete="shipping street-address"></label><br/>
+ <label>Address Level 2: <input id="address-level2-a" autocomplete="shipping address-level2"></label><br/>
+ <label>Address Level 1: <input id="address-level1-a" autocomplete="shipping address-level1"></label><br/>
+ <label>Postal Code: <input id="postal-code-a" autocomplete="shipping postal-code"></label><br/>
+ <label>Country: <input id="country-a" autocomplete="shipping country"></label><br/>
+
+ <br/>
+ <label>Street Address: <input id="street-address-b" autocomplete="billing street-address"></label><br/>
+ <label>Address Level 2: <input id="address-level2-b" autocomplete="billing address-level2"></label><br/>
+ <label>Address Level 1: <input id="address-level1-b" autocomplete="billing address-level1"></label><br/>
+ <label>Postal Code: <input id="postal-code-b" autocomplete="billing postal-code"></label><br/>
+ <label>Country: <input id="country-b" autocomplete="billing country"></label><br/>
+
+ <br/>
+ <label>Street Address: <input id="street-address-c" autocomplete="section-my street-address"></label><br/>
+ <label>Address Level 2: <input id="address-level2-c" autocomplete="section-my address-level2"></label><br/>
+ <label>Address Level 1: <input id="address-level1-c" autocomplete="section-my address-level1"></label><br/>
+ <label>Postal Code: <input id="postal-code-c" autocomplete="section-my postal-code"></label><br/>
+ <label>Country: <input id="country-c" autocomplete="section-my country"></label><br/>
+
+ <br/>
+ <label>Telephone: <input id="tel-a" autocomplete="work tel"></label><br/>
+ <label>Email: <input id="email-a" autocomplete="work email"></label><br/>
+ <br/>
+ <label>Telephone: <input id="tel-b" autocomplete="home tel"></label><br/>
+ <label>Email: <input id="email-b" autocomplete="home email"></label><br/>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+
+ <form>
+ <label>Name: <input autocomplete="name"></label><br/>
+ <label>Organization: <input autocomplete="organization"></label><br/>
+
+ <br/>
+ <label>Street Address: <input autocomplete="street-address"></label><br/>
+ <label>Address Level 2: <input autocomplete="address-level2"></label><br/>
+ <label>Address Level 1: <input autocomplete="address-level1"></label><br/>
+ <label>Postal Code: <input autocomplete="postal-code"></label><br/>
+ <label>Country: <input autocomplete="country"></label><br/>
+
+ <br/>
+ <label>Street Address: <input autocomplete="street-address"></label><br/>
+ <label>Address Level 2: <input autocomplete="address-level2"></label><br/>
+ <label>Address Level 1: <input autocomplete="address-level1"></label><br/>
+ <label>Postal Code: <input autocomplete="postal-code"></label><br/>
+ <label>Country: <input autocomplete="country"></label><br/>
+
+ <br/>
+ <label>Street Address: <input autocomplete="street-address"></label><br/>
+ <label>Address Level 2: <input autocomplete="address-level2"></label><br/>
+ <label>Address Level 1: <input autocomplete="address-level1"></label><br/>
+ <label>Postal Code: <input autocomplete="postal-code"></label><br/>
+ <label>Country: <input autocomplete="country"></label><br/>
+
+ <br/>
+ <label>Telephone: <input autocomplete="work tel"></label><br/>
+ <label>Email: <input autocomplete="work email"></label><br/>
+ <br/>
+ <label>Telephone: <input autocomplete="home tel"></label><br/>
+ <label>Email: <input autocomplete="home email"></label><br/>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Checkout – Best Buy</title>
+ </head>
+ <body>
+ <form name="frmSearch" action="https://www.bestbuy.com/site/searchpage.jsp" method="GET">
+ <input type="text" value="" name="st" maxlength="90" placeholder="Search Best Buy" id="gh-search-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
+ <input type="hidden" value="UTF-8" name="_dyncharset">
+ <input type="hidden" value="pcat17071" name="id">
+ <input type="hidden" value="page" name="type">
+ <input type="hidden" value="Global" name="sc">
+ <input type="hidden" value="1" name="cp">
+ <input type="hidden" value="" name="nrp">
+ <input type="hidden" value="" name="sp">
+ <input type="hidden" value="" name="qp">
+ <input type="hidden" value="n" name="list">
+ <input type="hidden" value="true" name="af">
+ <input type="hidden" value="y" name="iht">
+ <input type="hidden" value="All Categories" name="usc">
+ <input type="hidden" value="960" name="ks">
+ <input type="hidden" id="keys" value="keys" name="keys">
+ </form>
+ <form action="https://www-ssl.bestbuy.com/site/olspage.jsp?id=pcat17009&amp;type=page&amp;fastTrack=true" id="footer-email-form">
+ <label for="footerEmailSignup">GET THE LATEST DEALS &amp; MORE</label>
+ <input type="text" id="footerEmailSignup" name="email" placeholder="Enter E-Mail Address">
+ <input type="submit" value="Sign Up"
+title="Sign Up">
+ </form>
+ <form action="javascript://">
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.firstName">
+ <span>
+ <p>First Name</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.firstName" name="firstName" maxlength="29" value="">
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.lastName">
+ <span>
+ <p>Last Name</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.lastName" name="lastName" maxlength="30" value="">
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.street">
+ <span>
+ <p>Address</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.street" name="street" maxlength="35" value="">
+ </div>
+ </label>
+ </div>
+ <div>
+ <label id="fulfillment.fulfillmentGroups.0.fulfillment.address.city" for="fulfillment.fulfillmentGroups.0.fulfillment.address.city">
+ <span>
+ <p>City</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.city" name="city" maxlength="30" value="">
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.state">
+ <span>
+ <p>State</p>
+ </span>
+ <div>
+ <select id="fulfillment.fulfillmentGroups.0.fulfillment.address.state" name="state">
+ <option value="">Select a state</option>
+ <option value="AL">AL - Alabama</option>
+ <option value="AK">AK - Alaska</option>
+ <option value="AP">AP - Armed Forces Pacific</option>
+ <option value="AE">AE - Armed Force Europe</option>
+ <option value="AA">AA - Armed Forces America</option>
+ <option value="AZ">AZ - Arizona</option>
+ <option value="AR">AR - Arkansas</option>
+ <option value="CA">CA - California</option>
+ <option value="CO">CO - Colorado</option>
+ <option value="CT">CT - Connecticut</option>
+ <option value="DC">DC - Washington D.C.</option>
+ <option value="DE">DE - Delaware</option>
+ <option value="FL">FL - Florida</option>
+ <option value="GA">GA - Georgia</option>
+ <option value="GU">GU - Guam</option>
+ <option value="HI">HI - Hawaii</option>
+ <option value="ID">ID - Idaho</option>
+ <option value="IL">IL - Illinois</option>
+ <option value="IN">IN - Indiana</option>
+ <option value="IA">IA - Iowa</option>
+ <option value="KS">KS - Kansas</option>
+ <option value="KY">KY - Kentucky</option>
+ <option value="LA">LA - Louisiana</option>
+ <option value="ME">ME - Maine</option>
+ <option value="MD">MD - Maryland</option>
+ <option value="MA">MA - Massachusetts</option>
+ <option value="MI">MI - Michigan</option>
+ <option value="MN">MN - Minnesota</option>
+ <option value="MS">MS - Mississippi</option>
+ <option value="MO">MO - Missouri</option>
+ <option value="MT">MT - Montana</option>
+ <option value="NE">NE - Nebraska</option>
+ <option value="NV">NV - Nevada</option>
+ <option value="NH">NH - New Hampshire</option>
+ <option value="NJ">NJ - New Jersey</option>
+ <option value="NM">NM - New Mexico</option>
+ <option value="NY">NY - New York</option>
+ <option value="NC">NC - North Carolina</option>
+ <option value="ND">ND - North Dakota</option>
+ <option value="OH">OH - Ohio</option>
+ <option value="OK">OK - Oklahoma</option>
+ <option value="OR">OR - Oregon</option>
+ <option value="PA">PA - Pennsylvania</option>
+ <option value="RI">RI - Rhode Island</option>
+ <option value="SC">SC - South Carolina</option>
+ <option value="SD">SD - South Dakota</option>
+ <option value="TN">TN - Tennessee</option>
+ <option value="TX">TX - Texas</option>
+ <option value="UT">UT - Utah</option>
+ <option value="VT">VT - Vermont</option>
+ <option value="VA">VA - Virginia</option>
+ <option value="VI">VI - Virgin Islands</option>
+ <option value="WA">WA - Washington</option>
+ <option value="WV">WV - West Virginia</option>
+ <option value="WI">WI - Wisconsin</option>
+ <option value="WY">WY - Wyoming</option>
+ </select>
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.zipcode">
+ <span>
+ <p>ZIP Code</p>
+ </span>
+ <div>
+ <input type="tel" id="fulfillment.fulfillmentGroups.0.fulfillment.address.zipcode" name="zipcode" maxlength="5" value="">
+ </div>
+ </label>
+ </div>
+ </form>
+ <div role="search">
+ <div id="app">
+ <div>
+ <div>
+ <div>
+ <div>
+ <section>
+ <div>
+ <div>
+ <section>
+ <section>
+ <div>
+ <ul>
+ <li>
+ <label for="ispu-fulfillmentci779178028329">
+ <div>
+ <input type="radio" id="ispu-fulfillmentci779178028329" name="fulfillment-options-list__radio_0" value="">
+<i>
+</i>
+ </div>
+ <strong>
+ <span>Tomorrow</span>
+ <span>
+ <p> at a Best Buy store</p>
+ </span>
+ </strong>
+ <div>
+ <span>
+ <p>Store pick ups are usually ready within one hour and held for up to 8 days</p>
+ </span>
+ </div>
+ </label>
+ </li>
+ </ul>
+ </div>
+ <div>
+ <ul>
+ <li>
+ <label for="0losTwo Day">
+ <div>
+ <input type="radio" id="0losTwo Day" name="fulfillment-options-list__radio_0" value="">
+<i>
+</i>
+ </div>
+ <strong>
+<span>Wed, Mar 22</span>
+</strong>- Two Day Shipping
+ </label>
+ </li>
+ <li>
+ <label for="0losOne Day">
+ <div>
+ <input type="radio" id="0losOne Day" name="fulfillment-options-list__radio_0" value="">
+<i>
+</i>
+ </div>
+ <strong>
+<span>Tue, Mar 21</span>
+</strong>- One Day Shipping
+ </label>
+ </li>
+ <li>
+ <label for="0losSame Day">
+ <div>
+ <input type="radio" id="0losSame Day" name="fulfillment-options-list__radio_0" value="">
+<i>
+</i>
+ </div>
+ <strong>
+<span>Tomorrow</span>
+</strong>- Same Day Shipping
+ </label>
+ </li>
+ </ul>
+ </div>
+ </section>
+ <section>
+ <label for="save-for-billing-address-0">
+ <div>
+ <input type="checkbox" id="save-for-billing-address-0" value="">
+<i>
+</i>
+ </div>
+ <span>
+ <p>Save this as my billing address</p>
+ </span>
+ </label>
+ </section>
+ </section>
+ </div>
+ <div>
+ <section>
+ <div>
+ <label for="user.emailAddress">
+ <span>
+ <p>E-mail Address</p>
+ </span>
+ <div>
+ <input id="user.emailAddress" name="emailAddress" value="">
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="user.phone">
+ <span>
+ <p>Phone Number</p>
+ </span>
+ <div>
+ <input type="tel" id="user.phone" name="phone" maxlength="12" value="">
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="text-updates">
+ <div>
+ <input type="checkbox" id="text-updates" value="">
+ </div>
+ <span>
+ <p>Send me text notifications for my order</p>
+ </span>
+ </label>
+ </div>
+ </section>
+ </div>
+ </div>
+ </section>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Checkout – Best Buy</title>
+ </head>
+ <body>
+ <form name="frmSearch" action="https://www.bestbuy.com/site/searchpage.jsp" method="GET">
+ <label for="gh-search-input">Search Best Buy</label>
+ <input type="text" value="" name="st" maxlength="90" placeholder="Search Best Buy" id="gh-search-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
+ <input type="hidden" value="UTF-8" name="_dyncharset" />
+ <input type="hidden" value="pcat17071" name="id" />
+ <input type="hidden" value="page" name="type" />
+ <input type="hidden" value="Global" name="sc" />
+ <input type="hidden" value="1" name="cp" />
+ <input type="hidden" value="" name="nrp" />
+ <input type="hidden" value="" name="sp" />
+ <input type="hidden" value="" name="qp" />
+ <input type="hidden" value="n" name="list" />
+ <input type="hidden" value="true" name="af" />
+ <input type="hidden" value="y" name="iht" />
+ <input type="hidden" value="All Categories" name="usc" />
+ <input type="hidden" value="960" name="ks" />
+ <input type="hidden" id="keys" value="keys" name="keys" />
+ </form>
+ <form action="javascript://">
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.firstName">
+ <span>
+ <p>First Name</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.firstName" name="firstName" maxlength="29" value=""
+title="overall type: NAME_FIRST
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: First Name
+ parseable name: firstName
+ field signature: 1855613035
+ form signature: 670076259790528644"
+autofill-prediction="NAME_FIRST"
+/>
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.lastName">
+ <span>
+ <p>Last Name</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.lastName" name="lastName" maxlength="30" value=""
+title="overall type: NAME_LAST
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: Last Name
+ parseable name: lastName
+ field signature: 4163345999
+ form signature: 670076259790528644"
+autofill-prediction="NAME_LAST"
+/>
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.street">
+ <span>
+ <p>Address</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.street" name="street" maxlength="35" value=""
+title="overall type: ADDRESS_HOME_STREET_ADDRESS
+ server type: ADDRESS_HOME_STREET_ADDRESS
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address
+ parseable name: street
+ field signature: 3370790275
+ form signature: 670076259790528644"
+autofill-prediction="ADDRESS_HOME_STREET_ADDRESS"
+/>
+ </div>
+ </label>
+ </div>
+ <div>
+ <label id="fulfillment.fulfillmentGroups.0.fulfillment.address.city" for="fulfillment.fulfillmentGroups.0.fulfillment.address.city">
+ <span>
+ <p>City</p>
+ </span>
+ <div>
+ <input type="text" id="fulfillment.fulfillmentGroups.0.fulfillment.address.city" name="city" maxlength="30" value=""
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: City
+ parseable name: city
+ field signature: 2098554694
+ form signature: 670076259790528644"
+autofill-prediction="ADDRESS_HOME_CITY"
+/>
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.state">
+ <span>
+ <p>State</p>
+ </span>
+ <div>
+ <select id="fulfillment.fulfillmentGroups.0.fulfillment.address.state" name="state"
+title="overall type: ADDRESS_HOME_STATE
+ server type: ADDRESS_HOME_STATE
+ heuristic type: ADDRESS_HOME_STATE
+ label: State
+ parseable name: state
+ field signature: 1878375253
+ form signature: 670076259790528644"
+autofill-prediction="ADDRESS_HOME_STATE"
+/>
+ <option value="">Select a state</option>
+ <option value="AL">AL - Alabama</option>
+ <option value="AK">AK - Alaska</option>
+ <option value="AP">AP - Armed Forces Pacific</option>
+ <option value="AE">AE - Armed Forces Europe</option>
+ <option value="AA">AA - Armed Forces America</option>
+ <option value="AZ">AZ - Arizona</option>
+ <option value="AR">AR - Arkansas</option>
+ <option value="CA">CA - California</option>
+ <option value="CO">CO - Colorado</option>
+ <option value="CT">CT - Connecticut</option>
+ <option value="DC">DC - Washington D.C.</option>
+ <option value="DE">DE - Delaware</option>
+ <option value="FL">FL - Florida</option>
+ <option value="GA">GA - Georgia</option>
+ <option value="GU">GU - Guam</option>
+ <option value="HI">HI - Hawaii</option>
+ <option value="ID">ID - Idaho</option>
+ <option value="IL">IL - Illinois</option>
+ <option value="IN">IN - Indiana</option>
+ <option value="IA">IA - Iowa</option>
+ <option value="KS">KS - Kansas</option>
+ <option value="KY">KY - Kentucky</option>
+ <option value="LA">LA - Louisiana</option>
+ <option value="ME">ME - Maine</option>
+ <option value="MD">MD - Maryland</option>
+ <option value="MA">MA - Massachusetts</option>
+ <option value="MI">MI - Michigan</option>
+ <option value="MN">MN - Minnesota</option>
+ <option value="MS">MS - Mississippi</option>
+ <option value="MO">MO - Missouri</option>
+ <option value="MT">MT - Montana</option>
+ <option value="NE">NE - Nebraska</option>
+ <option value="NV">NV - Nevada</option>
+ <option value="NH">NH - New Hampshire</option>
+ <option value="NJ">NJ - New Jersey</option>
+ <option value="NM">NM - New Mexico</option>
+ <option value="NY">NY - New York</option>
+ <option value="NC">NC - North Carolina</option>
+ <option value="ND">ND - North Dakota</option>
+ <option value="OH">OH - Ohio</option>
+ <option value="OK">OK - Oklahoma</option>
+ <option value="OR">OR - Oregon</option>
+ <option value="PA">PA - Pennsylvania</option>
+ <option value="RI">RI - Rhode Island</option>
+ <option value="SC">SC - South Carolina</option>
+ <option value="SD">SD - South Dakota</option>
+ <option value="TN">TN - Tennessee</option>
+ <option value="TX">TX - Texas</option>
+ <option value="UT">UT - Utah</option>
+ <option value="VT">VT - Vermont</option>
+ <option value="VA">VA - Virginia</option>
+ <option value="VI">VI - Virgin Islands</option>
+ <option value="WA">WA - Washington</option>
+ <option value="WV">WV - West Virginia</option>
+ <option value="WI">WI - Wisconsin</option>
+ <option value="WY">WY - Wyoming</option>
+ </select>
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="fulfillment.fulfillmentGroups.0.fulfillment.address.zipcode">
+ <span>
+ <p>ZIP Code</p>
+ </span>
+ <div>
+ <input type="tel" id="fulfillment.fulfillmentGroups.0.fulfillment.address.zipcode" name="zipcode" maxlength="5" value=""
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP Code
+ parseable name: zipcode
+ field signature: 390262106
+ form signature: 670076259790528644"
+autofill-prediction="ADDRESS_HOME_ZIP"
+/>
+ </div>
+ </label>
+ </div>
+ </form>
+ <form action="https://www-ssl.bestbuy.com/site/olspage.jsp?id=pcat17009&amp;type=page&amp;fastTrack=true" id="footer-email-form">
+<label for="footerEmailSignup">GET THE LATEST DEALS &amp; MORE</label>
+ <input type="text" id="footerEmailSignup" name="email" placeholder="Enter E-Mail Address" />
+ <input type="submit" value="Sign Up"
+title="Sign Up" />
+ </form>
+ <div id="checkout-container">
+ <div id="app">
+ <div>
+ <div>
+ <div>
+ <div>
+ <section>
+ <div>
+ <div>
+ <section>
+ <section>
+ <div>
+ <ul>
+ <li>
+ <label for="ispu-fulfillmentci779178028329">
+ <div>
+ <input type="radio" id="ispu-fulfillmentci779178028329" name="fulfillment-options-list__radio_0" value="" />
+ </div>
+ <strong>
+ <span>Tomorrow</span>
+ <span>
+ <p> at a Best Buy store</p>
+ </span>
+ </strong>
+ <div>
+ <span>
+ <p>Store pick ups are usually ready within one hour and held for up to 8 days</p>
+ </span>
+ </div>
+ </label>
+ </li>
+ </ul>
+ </div>
+ <div>
+ <ul>
+ <li>
+ <label for="0losTwo Day">
+ <div>
+ <input type="radio" id="0losTwo Day" name="fulfillment-options-list__radio_0" value="" />
+ </div>
+ <strong>
+<span>Wed, Mar 22</span>
+</strong> - Two Day Shipping
+ <p>
+ Some items may arrive slower. See
+ <span>
+<span>
+<p>Order Summary</p>
+</span>
+</span> for details.
+ </p>
+ </label>
+ </li>
+ <li>
+ <label for="0losOne Day">
+ <div>
+ <input type="radio" id="0losOne Day" name="fulfillment-options-list__radio_0" value="" />
+ </div>
+ <strong>
+<span>Tue, Mar 21</span>
+</strong> - One Day Shipping
+ </label>
+ </li>
+ </ul>
+ </div>
+ </section>
+ <section>
+ <label for="save-for-billing-address-0">
+ <div>
+ <input type="checkbox" id="save-for-billing-address-0" value="" />
+ </div>
+ <span>
+ <p>Save this as my billing address</p>
+ </span>
+ </label>
+ </section>
+ </section>
+ </div>
+ <div>
+ <section>
+ <div>
+ <label for="user.emailAddress">
+ <span>
+ <p>E-mail Address</p>
+ </span>
+ <div>
+ <input id="user.emailAddress" name="emailAddress" value="" />
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="user.phone">
+ <span>
+ <p>Phone Number</p>
+ </span>
+ <div>
+ <input type="tel" id="user.phone" name="phone" maxlength="12" value="" />
+ </div>
+ </label>
+ </div>
+ <div>
+ <label for="text-updates">
+ <div>
+ <input type="checkbox" id="text-updates" value="" />
+ </div>
+ <span>
+ <p>Send me text notifications for my order</p>
+ </span>
+ </label>
+ </div>
+ </section>
+ </div>
+ </div>
+ </section>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Sign In to BestBuy.com</title>
+ </head>
+ <body>
+ <form action="https://www-ssl.bestbuy.com/" name="ciaSignOn" >
+ <div>
+ <label for="fld-e">E-Mail Address</label>
+ <input type="email" name="email_MAGIC_HASH_1" id="fld-e" required="" />
+ </div>
+ <div>
+ <label for="fld-p1">Password</label>
+ <div>
+ <input type="password" name="password_MAGIC_HASH_2" id="fld-p1" required="" />
+ </div>
+ </div>
+ <input type="hidden" name="Salmon" value="MAGIC_HASH_3" />
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>
+ Checkout
+ </title>
+ </head>
+ <body id="MasterPageBodyTag">
+ <form name="form1" method="post" action="https://www.cdw.com/shop/checkout/guest/BillingAndPayment.aspx" id="form1">
+ <div>
+ </div>
+ <div>
+ <input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="669B25B9">
+ <input type="hidden" name="__VIEWSTATEENCRYPTED" id="__VIEWSTATEENCRYPTED" value="">
+ </div>
+ <div>
+ <div>
+ <div>
+ </div>
+ <div>
+ <div>
+ </div>
+ <div id="newBillingAddressOptions">
+ <div>
+ <div>
+ <input value="" name="ctl00$ctl00$MainContentRoot$Body$addressOption" type="radio" id="sameAsShippingAddress" checked="checked"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Use my Shipping Address as my Billing Address
+ parseable name: addressOption
+ field signature: 825932642
+ form signature: 11231346808802434240"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label id="lbl1" for="sameAsShippingAddress">Use my Shipping Address as my Billing Address</label>
+ </div>
+ <div>
+ <input value="" name="ctl00$ctl00$MainContentRoot$Body$addressOption" type="radio" id="createNewAddress"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Enter Billing Address
+ parseable name: addressOption
+ field signature: 825932642
+ form signature: 11231346808802434240"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label id="lbl2" for="createNewAddress">Enter Billing Address</label>
+ </div>
+ </div>
+ </div>
+ <div id="newBillingAddress">
+ <fieldset>
+ <div>
+ <label for="firstName">First Name (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$firstName" type="text" id="firstName" maxlength="75"
+title="overall type: NAME_FIRST
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: First Name (required)
+ parseable name: ctl00$firstName
+ field signature: 759447197
+ form signature: 11231346808802434240"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ <div>
+ <label for="lastName">Last Name (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$lastName" type="text" id="lastName" maxlength="75"
+title="overall type: NAME_LAST
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: Last Name (required)
+ parseable name: ctl00$lastName
+ field signature: 2226109235
+ form signature: 11231346808802434240"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ <div>
+ <label for="company">Company</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$company" type="text" id="company" maxlength="100"
+title="overall type: COMPANY_NAME
+ server type: COMPANY_NAME
+ heuristic type: COMPANY_NAME
+ label: Company
+ parseable name: ctl00$company
+ field signature: 474096225
+ form signature: 11231346808802434240"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ <div>
+ <label for="address1">Address Line 1 (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$address1" type="text" id="address1" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address Line 1 (required)
+ parseable name: ctl00$address1
+ field signature: 3936848337
+ form signature: 11231346808802434240"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <div>
+ <label for="address2">Address Line 2 </label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$address2" type="text" id="address2" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address Line 2
+ parseable name: ctl00$address2
+ field signature: 3389805014
+ form signature: 11231346808802434240"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ <div>
+ <div>
+ <label for="city">City (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$city" type="text" id="city" maxlength="25"
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: City (required)
+ parseable name: ctl00$city
+ field signature: 794505091
+ form signature: 11231346808802434240"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ <div>
+ <label for="stateProvince">State (required)</label>
+ <select name="ctl00$ctl00$MainContentRoot$Body$ctl00$stateProvince" id="stateProvince"
+title="overall type: ADDRESS_HOME_STATE
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_STATE
+ label: State (required)
+ parseable name: ctl00$stateProvince
+ field signature: 548222440
+ form signature: 11231346808802434240"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option selected="selected" value="">Choose a state</option>
+ <option value="AL-US">AL-Alabama</option>
+ <option value="AK-US">AK-Alaska</option>
+ <option value="AS-AS">AS-American Samoa</option>
+ <option value="AZ-US">AZ-Arizona</option>
+ <option value="AR-US">AR-Arkansas</option>
+ <option value="AE-US">AE-Armed Forces Africa</option>
+ <option value="AA-US">AA-Armed Forces Americas</option>
+ <option value="AE-US">AE-Armed Forces Canada</option>
+ <option value="AE-US">AE-Armed Forces Europe</option>
+ <option value="AE-US">AE-Armed Forces Middle East</option>
+ <option value="AP-US">AP-Armed Forces Pacific</option>
+ <option value="CA-US">CA-California</option>
+ <option value="CO-US">CO-Colorado</option>
+ <option value="CT-US">CT-Connecticut</option>
+ <option value="DE-US">DE-Delaware</option>
+ <option value="DC-US">DC-District of Columbia</option>
+ <option value="FM-FM">FM-Federated States of Micronesia</option>
+ <option value="FL-US">FL-Florida</option>
+ <option value="GA-US">GA-Georgia</option>
+ <option value="GU-GU">GU-Guam</option>
+ <option value="HI-US">HI-Hawaii</option>
+ <option value="ID-US">ID-Idaho</option>
+ <option value="IL-US">IL-Illinois</option>
+ <option value="IN-US">IN-Indiana</option>
+ <option value="IA-US">IA-Iowa</option>
+ <option value="KS-US">KS-Kansas</option>
+ <option value="KY-US">KY-Kentucky</option>
+ <option value="LA-US">LA-Louisiana</option>
+ <option value="ME-US">ME-Maine</option>
+ <option value="MH-MH">MH-Marshall Islands</option>
+ <option value="MD-US">MD-Maryland</option>
+ <option value="MA-US">MA-Massachusetts</option>
+ <option value="MI-US">MI-Michigan</option>
+ <option value="MN-US">MN-Minnesota</option>
+ <option value="MS-US">MS-Mississippi</option>
+ <option value="MO-US">MO-Missouri</option>
+ <option value="MT-US">MT-Montana</option>
+ <option value="NE-US">NE-Nebraska</option>
+ <option value="NV-US">NV-Nevada</option>
+ <option value="NH-US">NH-New Hampshire</option>
+ <option value="NJ-US">NJ-New Jersey</option>
+ <option value="NM-US">NM-New Mexico</option>
+ <option value="NY-US">NY-New York</option>
+ <option value="NC-US">NC-North Carolina</option>
+ <option value="ND-US">ND-North Dakota</option>
+ <option value="MP-MP">MP-Norther Mariana Islands</option>
+ <option value="OH-US">OH-Ohio</option>
+ <option value="OK-US">OK-Oklahoma</option>
+ <option value="OR-US">OR-Oregon</option>
+ <option value="PA-US">PA-Pennsylvania</option>
+ <option value="PR-PR">PR-Puerto Rico</option>
+ <option value="PW-PW">PW-Palau</option>
+ <option value="RI-US">RI-Rhode Island</option>
+ <option value="SC-US">SC-South Carolina</option>
+ <option value="SD-US">SD-South Dakota</option>
+ <option value="TN-US">TN-Tennessee</option>
+ <option value="TX-US">TX-Texas</option>
+ <option value="UT-US">UT-Utah</option>
+ <option value="VT-US">VT-Vermont</option>
+ <option value="VI-US">VI-Virgin Islands</option>
+ <option value="VA-US">VA-Virginia</option>
+ <option value="WA-US">WA-Washington</option>
+ <option value="WV-US">WV-West Virginia</option>
+ <option value="WI-US">WI-Wisconsin</option>
+ <option value="WY-US">WY-Wyoming</option>
+ </select>
+ </div>
+ <div>
+ <label for="zipCode">ZIP Code (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$zipCode" type="text" id="zipCode" maxlength="5"
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP Code (required)
+ parseable name: ctl00$zipCode
+ field signature: 4227103349
+ form signature: 11231346808802434240"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ <div>
+ <label for="zipCodeExtn">ZIP Extn</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$zipCodeExtn" type="text" id="zipCodeExtn" maxlength="4"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: ZIP Extn
+ parseable name: ctl00$zipCodeExtn
+ field signature: 2328453303
+ form signature: 11231346808802434240"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ </div>
+ <div>
+ <div> The billing address above must match what appears on this credit card's statement.</div>
+ <div>
+ <div>
+ <label for="creditCardType">Card Type (required)</label>
+ <select name="ctl00$ctl00$MainContentRoot$Body$creditCardType" id="creditCardType"
+title="overall type: CREDIT_CARD_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_TYPE
+ label: Card Type (required)
+ parseable name: creditCardType
+ field signature: 4008988516
+ form signature: 11231346808802434240"
+autofill-prediction="CREDIT_CARD_TYPE"
+>
+ <option value="Select Type">Select Type</option>
+ <option value="American Express">American Express</option>
+ <option value="Discover Network">Discover Network</option>
+ <option value="MasterCard">MasterCard</option>
+ <option value="Visa">Visa</option>
+ </select>
+ </div>
+ <div>
+ <label for="creditCardNumber">Credit Card Number (req)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$creditCardNumber" type="text" id="creditCardNumber" maxlength="16" autocomplete="off"
+title="overall type: CREDIT_CARD_NUMBER
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_NUMBER
+ label: Credit Card Number (req)
+ parseable name: creditCardNumber
+ field signature: 466166649
+ form signature: 11231346808802434240"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ <input name="ctl00$ctl00$MainContentRoot$Body$creditCardNumber_en" type="hidden" id="creditCardNumber_en" keydelimiter="**" exponent="010001" clearonsubmit="true" >
+ </div>
+ <div>
+ <label for="expiryMonth">Expiration Date (req)</label>
+ <span>
+ <select name="ctl00$ctl00$MainContentRoot$Body$expiryMonth" id="expiryMonth"
+title="overall type: CREDIT_CARD_EXP_MONTH
+ server type: CREDIT_CARD_EXP_MONTH
+ heuristic type: CREDIT_CARD_EXP_MONTH
+ label: Expiration Date (req) CVV (req)
+ parseable name: expiryMonth
+ field signature: 1744226145
+ form signature: 11231346808802434240"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option value="">mm</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ <select name="ctl00$ctl00$MainContentRoot$Body$expiryYear" id="expiryYear"
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ label: Expiration Date (req)
+ parseable name: expiryYear
+ field signature: 3338586057
+ form signature: 11231346808802434240"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option value="">yy</option>
+ <option value="2017">17</option>
+ <option value="2018">18</option>
+ <option value="2019">19</option>
+ <option value="2020">20</option>
+ <option value="2021">21</option>
+ <option value="2022">22</option>
+ <option value="2023">23</option>
+ <option value="2024">24</option>
+ <option value="2025">25</option>
+ <option value="2026">26</option>
+ </select>
+ </span>
+ </div>
+ <div>
+ <label for="expiryMonth">CVV&nbsp;(req)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$CreditCardCvvText" type="text" id="CreditCardCvvText" maxlength="4"
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: CVV (req)
+ parseable name: CreditCardCvvText
+ field signature: 2577719477
+ form signature: 11231346808802434240"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ <div>
+ <i>
+</i>
+ <div>
+ <div>
+ <div>
+ <span>What is a CVV?</span>
+<br>
+ <span>For Visa, MasterCard &amp; Discover, the three digits on the back of your card.</span>
+<br>
+ <span> For American Express, the 4 digits on the front of your card.</span>
+<br>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="submit" name="ctl00$ctl00$MainContentRoot$Body$saveButton" value="Next" id="saveButton">
+ <div>
+<a
+title="Go to Verisign">
+ <img src="./Checkout-BillingPaymentInfo_files/verisign.gif" alt="Verisign Secured" border="0">
+</a>
+ </div>
+ <div>
+<a target="_blank"
+title="Go to BBB">
+ <img src="./Checkout-BillingPaymentInfo_files/BetterBusinessBureau-horizontal" alt="BBB Accredited Busines">
+</a>
+ </div>
+ </div>
+ </div>
+ <div>
+ <ul>
+ <li id="shippingAddressStep">
+ <a id="shippingAddressEdit">Edit</a>
+ <div id="shippingAddressStepDetails">
+ </div>
+ </li>
+ <li id="shippingMethodStep">
+ <a id="shippingMethodEdit">Edit</a>
+ <div id="shippingMethodStepDetails">
+ <div>Shipping Method</div>
+ <div>
+ <div id="shippingMethodName">UPS Ground (2-3 days)</div>
+ <div id="shippingMethodDesc">2-3 business days</div>
+ <div id="shippingMethodCost">$19.99</div>
+ </div>
+ </div>
+ </li>
+ <li id="billingAndPaymentStep">
+ <a id="billingAndPaymentEdit">Edit</a>
+ <div id="billingAndPaymentStepDetails">
+ <div>
+ Billing Address
+ </div>
+ <div>
+ <div>
+ <span id="billingAddressFirstName">
+</span>
+ <span id="billingAddressLastName">
+</span>
+ </div>
+ <div id="billingAddressEmail">
+</div>
+ <div id="billingAddressLine1">
+</div>
+ <div id="billingAddressLine2">
+</div>
+ <div>
+ <span id="billingAddressCity">
+</span>,
+ <span id="billingAddressState">
+</span>
+ <span id="billingAddressPostalCode">
+</span>
+ </div>
+ </div>
+ <div>Payment Method</div>
+ <div id="paymentMethod">
+</div>
+ </div>
+ </li>
+ <li>
+ </li>
+ </ul>
+ </div>
+ <input type="text" name="Representative" id="Representative" value=""
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Billing and Payment
+ parseable name: Representative
+ field signature: 716948211
+ form signature: 11231346808802434240"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <input id="__RequestVerificationTokencw" name="__RequestVerificationTokencw" type="hidden" >
+ </form>
+ <div>
+ <div>
+ <div>
+ </div>
+ <ul>
+ <li>
+ <a id="button-log-on">Account Log On</a>
+ <span>&nbsp;or&nbsp;</span>
+ <a tabindex="2" id="button-create-account">Create Account</a>
+ </li>
+ <li>
+<a id="button-cart">
+ <i>
+</i> Cart (<span id="headerCartCount">1</span>)
+ <span id="headerCartTotal"> - $6,568.99</span>
+</a>
+ </li>
+ </ul>
+ </div>
+ <input type="hidden" id="HdnFreeShippingProductCartIndicator" clientidmode="static" value="0">
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>
+ Logon Checkout
+ </title>
+ </head>
+ <body id="MasterPageBodyTag">
+ <form name="LogonFormServer" method="post" action="https://www.cdw.com/shop/eaccount/logon/logon.aspx?site=" id="LogonFormServer" autocomplete="off">
+ <div>
+ <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="">
+ <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="">
+ <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE">
+ </div>
+ <div>
+ <input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="C774B3FE">
+ <input type="hidden" name="__VIEWSTATEENCRYPTED" id="__VIEWSTATEENCRYPTED" value="">
+ </div>
+ <p>You don't need an account to place an order but you will have the option to create one after completing your purchase.</p>
+ <div id="gcoVisualCaptchaContainer">
+ <div id="divCaptcha" valign="top">
+ <div id="VisualCaptchaContainer">
+ <p>Click or touch the <span>House</span>
+</p>
+ <div>
+ <div>
+<img >
+</div>
+ </div>
+ <div>
+<a
+title="Refresh">
+</a>
+</div>
+ </div>
+ <input type="hidden" name="ctl01$ctl00$MainContentRoot$Body$guestCheckoutButton$hidVisualCaptchaToken" id="hidVisualCaptchaToken" value="72fcbb43-d2de-4d9f-8ba3-17d3df45888e">
+ <input type="hidden" name="ctl01$ctl00$MainContentRoot$Body$guestCheckoutButton$hidVisualCaptchaSelectedXAxis" id="hidVisualCaptchaSelectedXAxis" value="">
+ <input type="hidden" name="ctl01$ctl00$MainContentRoot$Body$guestCheckoutButton$hidVisualCaptchaSelectedYAxis" id="hidVisualCaptchaSelectedYAxis" value="">
+ <div>
+ <span id="valVisualCaptchaInvalid">
+<span>!</span> The validation code entered is incorrect</span>
+ </div>
+ </div>
+ <div>
+ <a id="guestbutton">Checkout as Guest</a>
+ <input type="submit" name="ctl01$ctl00$MainContentRoot$Body$guestCheckoutButton$guestCheckOutButton" value="Continue" id="guestCheckOutButton" disabled="disabled">
+ </div>
+ </div>
+ <br>
+ <div id="sitePolicy">
+ <div>
+ <a
+title="Go to Privacy Policy" target="_blank">Privacy Policy</a> | <a title="Go to Terms and Conditions" target="_blank">Terms and Conditions</a>
+ </div>
+ <a
+title="Go to Verisign">
+ <img src="./Logon Checkout_files/verisign.gif" border="0">
+ </a>
+ </div>
+ <input id="__RequestVerificationTokencw" name="__RequestVerificationTokencw" type="hidden">
+ </form>
+ <form name="LogonForm" id="LogonForm" method="post" action="https://www.cdw.com/shop/Eaccount/logon/LogOnProcessor.aspx?UI=CheckoutSimplifiedUI" autocomplete="off">
+ <div id="divLogon">
+ <section>
+ <div>
+ <span id="lblUserName">User Name</span>
+ <a tabindex="70">Forgot user name?</a>
+ <div id="divUserName">
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$UserName" type="text" id="UserName" tabindex="10" maxlength="50">
+ </div>
+ </div>
+ <div>
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$SavePassword" type="checkbox" id="SavePassword" tabindex="12" value="1">
+ <label for="SavePassword">
+ Remember my user name on this computer
+ <img id="question-image" src="./Logon Checkout_files/tooltip-question-mark.jpg"
+title="">
+</label>
+ <div id="remember-tooltip">
+ <img src="./Logon Checkout_files/remember-me-tooltip.jpg" usemap="#closepopup">
+ <map name="closepopup" id="closepopup">
+ <area alt=""
+title="" shape="circle" coords="368,23,15.5">
+ </map>
+ </div>
+ </div>
+ <div>
+ <span id="lblUserPass">Password</span>
+ <a tabindex="80">Forgot password?</a>
+ <div id="divPassword">
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$UserPassword" type="password" id="UserPassword" tabindex="11" maxlength="50">
+ </div>
+ </div>
+ <div id="divCaptcha" valign="top">
+ </div>
+ <div id="DivInvalidCredentialsErrorMessage">
+ <span>!</span> You have entered an invalid username and/or password. Please re-enter your information.
+ </div>
+ <div id="DivInvalidCaptcha">
+ <span>!</span>
+ <span id="CaptchaErrorMessage">
+</span>
+ </div>
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$LogOnButton" type="submit" id="LogOnButton" tabindex="13" value="Log On" border="0">
+ </section>
+ </div>
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$WebSite" type="hidden" id="WebSite">
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$Target" type="hidden" id="Target" value="/shop/Checkout/ValidateCheckout.aspx?Standard=1&amp;cm_re=CRT-_-PZ-_-SC+Standard+Checkout+Button">
+ <input name="ctl01$ctl00$MainContentRoot$Body$LogonControl$ErrorCount" type="hidden" id="ErrorCount">
+ <span id="tagManEventControl">
+</span>
+ </form>
+ <div>
+ <input type="hidden" id="HdnFreeShippingProductCartIndicator" clientidmode="static" value="0">
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>
+ Checkout
+ </title>
+ </head>
+ <body id="MasterPageBodyTag">
+ <form name="form1" method="post" action="https://www.cdw.com/shop/checkout/guest/ShippingAddress.aspx" id="form1">
+ <div>
+ <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="">
+ <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="">
+ <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE">
+ </div>
+ <div>
+ <input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR">
+ <input type="hidden" name="__VIEWSTATEENCRYPTED" id="__VIEWSTATEENCRYPTED" value="">
+ </div>
+ <div>
+ <div>
+ <div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="firstName">First Name (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$firstName" type="text" id="firstName" maxlength="75"
+title="overall type: NAME_FIRST
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: First Name (required)
+ parseable name: tl00$firstName
+ field signature: 759447197
+ form signature: 7628530229511417656"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ <div>
+ <label for="lastName">Last Name (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$lastName" type="text" id="lastName" maxlength="75"
+title="overall type: NAME_LAST
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: Last Name (required)
+ parseable name: tl00$lastName
+ field signature: 2226109235
+ form signature: 7628530229511417656"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ <div>
+ <label for="company">Company</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$company" type="text" id="company" maxlength="100"
+title="overall type: COMPANY_NAME
+ server type: COMPANY_NAME
+ heuristic type: COMPANY_NAME
+ label: Company
+ parseable name: tl00$company
+ field signature: 474096225
+ form signature: 7628530229511417656"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ <div>
+ <label for="address1">Address Line 1 (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$address1" type="text" id="address1" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address Line 1 (required)
+ parseable name: tl00$address1
+ field signature: 3936848337
+ form signature: 7628530229511417656"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <div>
+ <label for="address2">Address Line 2 </label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$address2" type="text" id="address2" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address Line 2
+ parseable name: tl00$address2
+ field signature: 3389805014
+ form signature: 7628530229511417656"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ <div>
+ <div>
+ <label for="city">City (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$city" type="text" id="city" maxlength="25"
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: City (required)
+ parseable name: tl00$city
+ field signature: 794505091
+ form signature: 7628530229511417656"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ <div>
+ <label for="stateProvince">State (required)</label>
+ <select name="ctl00$ctl00$MainContentRoot$Body$ctl00$stateProvince" id="stateProvince"
+title="overall type: ADDRESS_HOME_STATE
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_STATE
+ label: State (required)
+ parseable name: tl00$stateProvince
+ field signature: 548222440
+ form signature: 7628530229511417656"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option selected="selected" value="">Choose a state</option>
+ <option value="AL-US">AL-Alabama</option>
+ <option value="AK-US">AK-Alaska</option>
+ <option value="AS-AS">AS-American Samoa</option>
+ <option value="AZ-US">AZ-Arizona</option>
+ <option value="AR-US">AR-Arkansas</option>
+ <option value="AE-US">AE-Armed Forces Africa</option>
+ <option value="AA-US">AA-Armed Forces Americas</option>
+ <option value="AE-US">AE-Armed Forces Canada</option>
+ <option value="AE-US">AE-Armed Forces Europe</option>
+ <option value="AE-US">AE-Armed Forces Middle East</option>
+ <option value="AP-US">AP-Armed Forces Pacific</option>
+ <option value="CA-US">CA-California</option>
+ <option value="CO-US">CO-Colorado</option>
+ <option value="CT-US">CT-Connecticut</option>
+ <option value="DE-US">DE-Delaware</option>
+ <option value="DC-US">DC-District of Columbia</option>
+ <option value="FM-FM">FM-Federated States of Micronesia</option>
+ <option value="FL-US">FL-Florida</option>
+ <option value="GA-US">GA-Georgia</option>
+ <option value="GU-GU">GU-Guam</option>
+ <option value="HI-US">HI-Hawaii</option>
+ <option value="ID-US">ID-Idaho</option>
+ <option value="IL-US">IL-Illinois</option>
+ <option value="IN-US">IN-Indiana</option>
+ <option value="IA-US">IA-Iowa</option>
+ <option value="KS-US">KS-Kansas</option>
+ <option value="KY-US">KY-Kentucky</option>
+ <option value="LA-US">LA-Louisiana</option>
+ <option value="ME-US">ME-Maine</option>
+ <option value="MH-MH">MH-Marshall Islands</option>
+ <option value="MD-US">MD-Maryland</option>
+ <option value="MA-US">MA-Massachusetts</option>
+ <option value="MI-US">MI-Michigan</option>
+ <option value="MN-US">MN-Minnesota</option>
+ <option value="MS-US">MS-Mississippi</option>
+ <option value="MO-US">MO-Missouri</option>
+ <option value="MT-US">MT-Montana</option>
+ <option value="NE-US">NE-Nebraska</option>
+ <option value="NV-US">NV-Nevada</option>
+ <option value="NH-US">NH-New Hampshire</option>
+ <option value="NJ-US">NJ-New Jersey</option>
+ <option value="NM-US">NM-New Mexico</option>
+ <option value="NY-US">NY-New York</option>
+ <option value="NC-US">NC-North Carolina</option>
+ <option value="ND-US">ND-North Dakota</option>
+ <option value="MP-MP">MP-Norther Mariana Islands</option>
+ <option value="OH-US">OH-Ohio</option>
+ <option value="OK-US">OK-Oklahoma</option>
+ <option value="OR-US">OR-Oregon</option>
+ <option value="PA-US">PA-Pennsylvania</option>
+ <option value="PR-PR">PR-Puerto Rico</option>
+ <option value="PW-PW">PW-Palau</option>
+ <option value="RI-US">RI-Rhode Island</option>
+ <option value="SC-US">SC-South Carolina</option>
+ <option value="SD-US">SD-South Dakota</option>
+ <option value="TN-US">TN-Tennessee</option>
+ <option value="TX-US">TX-Texas</option>
+ <option value="UT-US">UT-Utah</option>
+ <option value="VT-US">VT-Vermont</option>
+ <option value="VI-US">VI-Virgin Islands</option>
+ <option value="VA-US">VA-Virginia</option>
+ <option value="WA-US">WA-Washington</option>
+ <option value="WV-US">WV-West Virginia</option>
+ <option value="WI-US">WI-Wisconsin</option>
+ <option value="WY-US">WY-Wyoming</option>
+ </select>
+ </div>
+ <div>
+ <label for="zipCode">ZIP Code (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$zipCode" type="text" id="zipCode" maxlength="5"
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP Code (required)
+ parseable name: tl00$zipCode
+ field signature: 4227103349
+ form signature: 7628530229511417656"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ <div>
+ <label for="zipCodeExtn">ZIP Extn</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$ctl00$zipCodeExtn" type="text" id="zipCodeExtn" maxlength="4"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: ZIP Extn
+ parseable name: tl00$zipCodeExtn
+ field signature: 2328453303
+ form signature: 7628530229511417656"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>We will only contact you about your order and shipping.</div>
+ <div>
+ <div>
+ <label for="contactEmail">Email (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$contactEmail" type="text" id="contactEmail" maxlength="75"
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: Email (required)
+ parseable name: ontactEmail
+ field signature: 123947042
+ form signature: 7628530229511417656"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ <div>
+ <label for="contactPhoneNumber">Phone (required)</label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$contactPhoneNumber" type="text" id="contactPhoneNumber" maxlength="15"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER
+ server type: PHONE_HOME_CITY_AND_NUMBER
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: Phone (required)
+ parseable name: ontactPhoneNumber
+ field signature: 1588916982
+ form signature: 7628530229511417656"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </div>
+ <div>
+ <label for="contactPhoneExtension">Extn </label>
+ <input name="ctl00$ctl00$MainContentRoot$Body$contactPhoneExtension" type="text" id="contactPhoneExtension" maxlength="5"
+title="overall type: PHONE_HOME_CITY_CODE
+ server type: PHONE_HOME_CITY_CODE
+ heuristic type: PHONE_HOME_EXTENSION
+ label: Extn
+ parseable name: ontactPhoneExtension
+ field signature: 1782290665
+ form signature: 7628530229511417656"
+autofill-prediction="PHONE_HOME_CITY_CODE"
+>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <button id="saveButton" type="button">Next</button>
+ <input type="text" name="Representative" id="Representative" value=""
+title="overall type: ADDRESS_HOME_STREET_ADDRESS
+ server type: ADDRESS_HOME_STREET_ADDRESS
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Shipping Address Next
+ parseable name: Representative
+ field signature: 716948211
+ form signature: 7628530229511417656"
+autofill-prediction="ADDRESS_HOME_STREET_ADDRESS"
+>
+ </div>
+ <div>
+ <ul>
+ <li id="shippingAddressStep">
+ <a id="shippingAddressEdit">Edit</a>
+ <div id="shippingAddressStepDetails">
+ <div>
+ Address:
+ </div>
+ <div>
+ <div>
+ <span id="shippingAddressFirstName">
+</span>
+ <span id="shippingAddressLastName">
+</span>
+ </div>
+ <div>
+ <span id="shippingCompany">
+</span>
+ </div>
+ <div id="shippingAddressLine1">
+</div>
+ <div id="shippingAddressLine2">
+</div>
+ <div>
+ <span id="shippingAddressCity">
+</span>,
+ <span id="shippingAddressState">
+</span>
+ <span id="shippingAddressPostalCode">
+</span>
+ </div>
+ </div>
+ <div>
+ <div>
+ Contact Info:
+ </div>
+ <div>
+ <div>
+ <span id="contactEmail">
+</span>
+ </div>
+ <div>
+ <span id="contactPhone">
+</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li id="shippingMethodStep">
+ <a id="shippingMethodEdit">Edit</a>
+ <div id="shippingMethodStepDetails">
+ <div>Shipping Method</div>
+ <div>
+ <div id="shippingMethodName">-</div>
+ <div id="shippingMethodDesc">-</div>
+ <div id="shippingMethodCost">-</div>
+ </div>
+ </div>
+ </li>
+ <li id="billingAndPaymentStep">
+ <a id="billingAndPaymentEdit">Edit</a>
+ <div id="billingAndPaymentStepDetails">
+ <div>
+ Billing Address
+ </div>
+ <div>
+ <div>
+ <span id="billingAddressFirstName">
+</span>
+ <span id="billingAddressLastName">
+</span>
+ </div>
+ <div id="billingAddressEmail">
+</div>
+ <div id="billingAddressLine1">
+</div>
+ <div id="billingAddressLine2">
+</div>
+ <div>
+ <span id="billingAddressCity">
+</span>,
+ <span id="billingAddressState">
+</span>
+ <span id="billingAddressPostalCode">
+</span>
+ </div>
+ </div>
+ <div>Payment Method</div>
+ <div id="paymentMethod">
+</div>
+ </div>
+ </li>
+ <li>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <input id="__RequestVerificationTokencw" name="__RequestVerificationTokencw" type="hidden">
+ </form>
+ <div>
+ <input type="hidden" id="HdnFreeShippingProductCartIndicator" clientidmode="static" value="0">
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta feature="9.1.4">
+ <title>Costco - Payment</title>
+ <meta name="currentBuildNumber" content="3.0.29057.0">
+ <meta name="ServerName" content="www.costco.com ">
+ <meta name="LocalAddress" content="xxx.xxx.xxx.48">
+ <meta name="LocalName" content="TP26">
+ </head>
+ <body>
+ <form name="CheckoutPaymentForm" id="CheckoutPaymentForm" method="post" action="https://www.costco.com/CostcoBillingPayment">
+ <input type="hidden" name="selfAddressId" id="hiddenSelfAddressId" value="">
+ <input type="hidden" id="billHiddenInput0" name="billHideenInput" value="">
+ <input type="hidden" id="billAddrId" name="billAddrId" value="">
+ <input type="hidden" id="membershipNumber" name="membershipNum">
+ <input type="hidden" name="orderItemsCount" value="1">
+ <input type="hidden" value="" id="selectedAddressId" name="selectedAddressId">
+ <input type="hidden" name="orderId" value="644156669" id="WC_CheckoutPaymentsAndBillingAddressf_orderId">
+ <input type="hidden" name="storeId" value="10301" id="CheckoutPayment_inputs_1">
+ <input type="hidden" name="catalogId" value="10701" id="CheckoutPayment_inputs_2">
+ <input type="hidden" name="langId" value="-1" id="CheckoutPayment_inputs_3">
+ <input type="hidden" name="curr_year" value="2017" id="CheckoutPayment_inputs_5">
+ <input type="hidden" name="curr_month" value="3" id="CheckoutPayment_inputs_6">
+ <input type="hidden" name="curr_date" value="19" id="CheckoutPayment_inputs_7">
+ <input type="hidden" name="URL" value="OrderPrepare?URL=CheckoutReviewView">
+ <input type="hidden" name="xCreditCardId" value="">
+ <input type="hidden" name="ccLastFour" value="">
+ <input type="hidden" name="checkCCValue" value="false">
+ <input type="hidden" name="backURL" value="">
+ <input type="hidden" name="piAmount" value="84.99000">
+ <input type="hidden" name="cardNumberValueHolder" value="">
+ <input type="hidden" name="authToken" value="312">
+ <div>
+ <label for="billMeLater">
+ <input type="radio" checked="checked" id="billMeLater" name="billMeLater" value="no"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Payment Method
+ parseable name: billMeLater
+ field signature: 1700925171
+ form signature: 4979691664972472743"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </label>
+ <br>
+ <div>
+ <div>
+ <label for="payMethodId">Card Type</label>
+ <select name="payMethodId" id="payMethodId"
+title="overall type: CREDIT_CARD_TYPE
+ server type: CREDIT_CARD_TYPE
+ heuristic type: CREDIT_CARD_TYPE
+ label: Card Type
+ parseable name: payMethodId
+ field signature: 3668211827
+ form signature: 4979691664972472743"
+autofill-prediction="CREDIT_CARD_TYPE"
+>
+ <option value="Costco Credit Card">Costco Credit Card</option>
+ <option value="Discover">Discover</option>
+ <option value="Master Card">MasterCard</option>
+ <option value="VISA" selected="selected">VISA</option>
+ </select>
+ </div>
+ <div>
+ <label for="account">Card number</label>
+ <input
+title="overall type: CREDIT_CARD_NUMBER
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_NUMBER
+ label: Card number
+ parseable name: account
+ field signature: 3715653537
+ form signature: 4979691664972472743" type="text" id="account" name="account" value="" autocomplete="off"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="expire_month">Expiration Date</label>
+ <select
+title="overall type: CREDIT_CARD_EXP_MONTH
+ server type: CREDIT_CARD_EXP_MONTH
+ heuristic type: CREDIT_CARD_EXP_MONTH
+ label: Expiration Date
+ parseable name: expire_month
+ field signature: 3078539387
+ form signature: 4979691664972472743" id="expire_month" name="expire_month"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option value="Month">Month</option>
+ <option value="01">1</option>
+ <option value="02">2</option>
+ <option value="03">3</option>
+ <option value="04">4</option>
+ <option value="05">5</option>
+ <option value="06">6</option>
+ <option value="07">7</option>
+ <option value="08">8</option>
+ <option value="09">9</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ <select
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ server type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ heuristic type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ label: Expiration Date
+ parseable name: expire_year
+ field signature: 2521850425
+ form signature: 4979691664972472743" name="expire_year"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option value="Year">Year</option>
+ <option value="2017">2017</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ </select>
+ </div>
+ <div>
+ <label for="cc_cvc">CVV Code</label>
+ <input
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: CVV Code
+ parseable name: cc_cvc
+ field signature: 1956128288
+ form signature: 4979691664972472743" type="text" id="cc_cvc" name="cc_cvc" value="" maxlength="4" autocomplete="off"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ <span>&nbsp;<span>?</span>
+</span>
+ </div>
+ </div>
+ <div>
+ <label for="cc_nameoncard">Cardholder Name</label>
+ <input
+title="overall type: CREDIT_CARD_NAME_FULL
+ server type: CREDIT_CARD_NAME_FULL
+ heuristic type: CREDIT_CARD_NAME_FULL
+ label: Cardholder Name
+ parseable name: cc_nameoncard
+ field signature: 1086986730
+ form signature: 4979691664972472743" type="text" id="cc_nameoncard" name="cc_nameoncard" value="" autocomplete="off"
+autofill-prediction="CREDIT_CARD_NAME_FULL"
+>
+ </div>
+ <div>
+ <input
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save this as my default payment card
+ parseable name: save_CC
+ field signature: 3060743727
+ form signature: 4979691664972472743" type="checkbox" name="save_CC" id="save_CC"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="save_CC">Save this as my default payment card</label>
+ </div>
+ </div>
+ </form>
+ <form name="CashCardForm" method="post" action="https://www.costco.com/CostcoCashCardProcess" id="CashCardForm">
+ <input type="hidden" name="storeId" value="10301">
+ <input type="hidden" name="langId" value="-1">
+ <input type="hidden" name="orderId" value="644156669">
+ <input type="hidden" name="catalogId" value="10701">
+ <input type="hidden" name="addressId" value="">
+ <input type="hidden" name="cc_payMethodId" value="CostcoCashCard">
+ <input type="hidden" name="action" value="">
+ <input type="hidden" name="URL" value="CheckoutPaymentView">
+ <input type="hidden" name="account" value="">
+ <input type="hidden" name="expire_month" value="">
+ <input type="hidden" name="expire_year" value="">
+ <input type="hidden" name="cc_nameoncard" value="">
+ <input type="hidden" name="payMethodId" value="">
+ <input type="hidden" name="xCreditCardId" value="">
+ <input type="hidden" name="ccLastFour" value="">
+ <input type="hidden" name="authToken" value="312404731%2cKsqvty%2bpMJ%2bCAl3XeIkCxSEgLa4%3d">
+ <div>
+ <div>
+ <label for="cash_account">Costco Cash Card Number</label>
+ <input
+title="Costco Cash Card Number" type="text" id="cash_account" name="cash_account" value="" maxlength="19" autocomplete="off">
+ </div>
+ <div>
+ <label for="cash_pin">PIN</label>
+ <input
+title="PIN" type="password" id="cash_pin" name="cash_pin" value="" maxlength="8" autocomplete="off">
+ <span>&nbsp;<span>?</span>
+</span>
+ <div>
+ <span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;More Info - Costco Cash Card</span>
+ <span>
+ <i>Costco Cash</i> Card Number</span>
+ <span>This image highlights the unique number</span>
+ <img src="./Costco - Payment_files/cashcard-us.gif" alt="cashcard-us.gif">
+ <span>used to identify your <i>Costco Cash</i> card.</span>
+ <span> Pin Number</span>
+ <span>This number is used to access your</span>
+ <span>
+ <i>Costco Cash</i> card. The image shows</span>
+ <span>where this number is located.</span>
+ <span>@ 1998-2016 Costco Wholesale Corporation. All rights reserved.</span>
+ </div>
+ </div>
+ </div>
+ </form>
+ <form name="RefreshBilling" method="post" action="https://www.costco.com/CostcoBillingPayment" id="RefreshBilling">
+ <input type="hidden" name="storeId" value="10301">
+ <input type="hidden" name="langId" value="-1">
+ <input type="hidden" name="orderId" value="644156669">
+ <input type="hidden" name="catalogId" value="10701">
+ <input type="hidden" name="actionType" value="refresh">
+ <input type="hidden" name="authToken" value="312404731%2cKsqvty%2bpMJ%2bCAl3XeIkCxSEgLa4%3d">
+ <input type="hidden" name="deviceId" value="">
+ </form>
+ <form name="PromotionCodeForm" method="post" action="https://www.costco.com/CostcoManagePromotionCmd" id="PromotionCodeForm">
+ <input type="hidden" name="storeId" value="10301">
+ <input type="hidden" name="langId" value="-1">
+ <input type="hidden" name="orderId" value="644156669">
+ <input type="hidden" name="catalogId" value="10701">
+ <input type="hidden" name="taskType" value="A">
+ <input type="hidden" name="URL" value="OrderCalculate?updatePrices=1&amp;calculationUsageId=-1&amp;URL=OrderPrepare?URL=CostcoPostPromotionCodeAddRemove&amp;orderId=.">
+ <input type="hidden" name="errorViewName" value="CheckoutPaymentView">
+ <input type="hidden" name="account" value="">
+ <input type="hidden" name="expire_month" value="">
+ <input type="hidden" name="expire_year" value="">
+ <input type="hidden" name="cc_nameoncard" value="">
+ <input type="hidden" name="payMethodId" value="">
+ <input type="hidden" name="xCreditCardId" value="">
+ <input type="hidden" name="ccLastFour" value="">
+ <input type="hidden" name="checkCCValue" value="false">
+ <input type="hidden" id="billHiddenInput0" name="billHideenInput" value="">
+ <input type="hidden" id="billAddrId" name="billAddrId" value="">
+ <input type="hidden" name="addressId" value="">
+ <input type="hidden" name="cc_cvc" value="">
+ <input type="hidden" name="piAmount" value="84.99000">
+ <div>
+ <label for="PromotionCodeForm_1">Promo Code</label>
+ <input
+title="Promo Code" type="text" size="10" name="promoCode" id="PromotionCodeForm_1" value="">
+ </div>
+ </form>
+ <form id="AddressFormModal-Form" name="AddressFormModal-Form" autocomplete="on" method="post">
+ <div>
+ <input type="hidden" name="addressType" value="B">
+ <p id="addressFormModalRequired" tabindex="-1">
+<span>*</span> Required fields</p>
+ <div id="personName">
+ <div>
+ <label for="addressFormModalFirstName">FIRST NAME<span>*</span>
+</label>
+ <input id="addressFormModalFirstName" name="addressFormModalFirstName"
+title="overall type: NAME_FIRST
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: FIRST NAME*
+ parseable name: addressFormModalFirstName
+ field signature: 2222266781
+ form signature: 8397269939060577503" type="text" maxlength="40"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ <div>
+ <label for="addressFormModalMiddleInitial">M.I.</label>
+ <input id="addressFormModalMiddleInitial" name="addressFormModalMiddleInitial"
+title="overall type: NAME_MIDDLE_INITIAL
+ server type: NAME_MIDDLE_INITIAL
+ heuristic type: NAME_MIDDLE_INITIAL
+ label: M.I.
+ parseable name: addressFormModalMiddleInitial
+ field signature: 3540652809
+ form signature: 8397269939060577503" type="text" maxlength="1"
+autofill-prediction="NAME_MIDDLE_INITIAL"
+>
+ </div>
+ <div>
+ <label for="addressFormModalLastName">LAST NAME<span>*</span>
+</label>
+ <input id="addressFormModalLastName" name="addressFormModalLastName"
+title="overall type: NAME_LAST
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: LAST NAME*
+ parseable name: addressFormModalLastName
+ field signature: 4218996568
+ form signature: 8397269939060577503" type="text" maxlength="40"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormModalCompany">COMPANY NAME</label>
+ <input id="addressFormModalCompany" name="addressFormModalCompany" type="text" maxlength="40"
+title="overall type: COMPANY_NAME
+ server type: COMPANY_NAME
+ heuristic type: COMPANY_NAME
+ label: COMPANY NAME
+ parseable name: addressFormModalCompany
+ field signature: 1845178698
+ form signature: 8397269939060577503"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ <div>
+ <label for="addressFormModalCountry">COUNTRY<span>*</span>
+</label>
+ <select id="addressFormModalCountry" name="addressFormModalCountry"
+title="overall type: ADDRESS_HOME_COUNTRY
+ server type: ADDRESS_HOME_COUNTRY
+ heuristic type: ADDRESS_HOME_COUNTRY
+ label: COUNTRY*
+ parseable name: addressFormModalCountry
+ field signature: 4052501735
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_COUNTRY"
+>
+ <option value="CA">Canada</option>
+ <option value="US">United States</option>
+ </select>
+ </div>
+ <div id="streetAddress">
+ <legend>
+ <label for="addressFormModalAddressLine1">STREET ADDRESS<span>*</span>
+</label>
+ </legend>
+ <div>
+ <input id="addressFormModalAddressLine1" name="addressFormModalAddressLine1" placeholder="Address Line 1" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: STREET ADDRESS*
+ parseable name: addressFormModalAddressLine1
+ field signature: 1532865404
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <div>
+ <input id="addressFormModalAddressLine2" name="addressFormModalAddressLine2" placeholder="Address Line 2" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address Line 2
+ parseable name: addressFormModalAddressLine2
+ field signature: 2315514959
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ </div>
+ <div id="city">
+ <label for="addressFormModalCity">CITY<span>*</span>
+</label>
+ <input id="addressFormModalCity" name="addressFormModalCity" type="text" maxlength="40"
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: CITY*
+ parseable name: addressFormModalCity
+ field signature: 4130865920
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ <div id="stateAndZip">
+ <div>
+ <label for="addressFormModalState">STATE / PROVINCE<span>*</span>
+</label>
+ <select id="addressFormModalState" name="addressFormModalState"
+title="overall type: ADDRESS_HOME_STATE
+ server type: ADDRESS_HOME_STATE
+ heuristic type: ADDRESS_HOME_STATE
+ label: STATE / PROVINCE*
+ parseable name: addressFormModalState
+ field signature: 4026908515
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="NO_STATE_TYPE_SELECTED" selected="selected">* Select</option>
+ <option value="Aa">AA - Armed Forces America</option>
+ <option value="Ae">AE - Armed Forces Europe</option>
+ <option value="AL">Alabama</option>
+ <option value="AK">Alaska</option>
+ <option value="Ap">AP - Armed Forces Pacific</option>
+ <option value="AZ">Arizona</option>
+ <option value="AR">Arkansas</option>
+ <option value="CA">California</option>
+ <option value="CO">Colorado</option>
+ <option value="CT">Connecticut</option>
+ <option value="DE">Delaware</option>
+ <option value="DC">District of Columbia</option>
+ <option value="FL">Florida</option>
+ <option value="GA">Georgia</option>
+ <option value="HI">Hawaii</option>
+ <option value="ID">Idaho</option>
+ <option value="IL">Illinois</option>
+ <option value="IN">Indiana</option>
+ <option value="IA">Iowa</option>
+ <option value="KS">Kansas</option>
+ <option value="KY">Kentucky</option>
+ <option value="LA">Louisiana</option>
+ <option value="ME">Maine</option>
+ <option value="MD">Maryland</option>
+ <option value="MA">Massachusetts</option>
+ <option value="MI">Michigan</option>
+ <option value="MN">Minnesota</option>
+ <option value="MS">Mississippi</option>
+ <option value="MO">Missouri</option>
+ <option value="MT">Montana</option>
+ <option value="NE">Nebraska</option>
+ <option value="NV">Nevada</option>
+ <option value="NH">New Hampshire</option>
+ <option value="NJ">New Jersey</option>
+ <option value="NM">New Mexico</option>
+ <option value="NY">New York</option>
+ <option value="NC">North Carolina</option>
+ <option value="ND">North Dakota</option>
+ <option value="OH">Ohio</option>
+ <option value="OK">Oklahoma</option>
+ <option value="OR">Oregon</option>
+ <option value="PA">Pennsylvania</option>
+ <option value="PR">Puerto Rico</option>
+ <option value="RI">Rhode Island</option>
+ <option value="SC">South Carolina</option>
+ <option value="SD">South Dakota</option>
+ <option value="TN">Tennessee</option>
+ <option value="TX">Texas</option>
+ <option value="UT">Utah</option>
+ <option value="VT">Vermont</option>
+ <option value="VA">Virginia</option>
+ <option value="WA">Washington</option>
+ <option value="WV">West Virginia</option>
+ <option value="WI">Wisconsin</option>
+ <option value="WY">Wyoming</option>
+ </select>
+ </div>
+ <div>
+ <label for="addressFormModalZip">ZIP / POSTAL CODE<span>*</span>
+</label>
+ <input id="addressFormModalZip" name="addressFormModalZip" type="text" maxlength="10"
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP / POSTAL CODE*
+ parseable name: addressFormModalZip
+ field signature: 2383002781
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </div>
+ <div id="phoneNumber">
+ <label for="addressFormModalPhoneNumber">PHONE NUMBER<span>*</span>
+</label>
+ <input id="addressFormModalPhoneNumber" name="addressFormModalPhoneNumber" type="text" maxlength="32"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER
+ server type: PHONE_HOME_CITY_AND_NUMBER
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: PHONE NUMBER*
+ parseable name: addressFormModalPhoneNumber
+ field signature: 1884423068
+ form signature: 8397269939060577503"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </div>
+ <div id="email">
+ <label for="addressFormModalEmail" id="addressFormModalEmailLabel">EMAIL<span>*</span>
+</label>
+ <input id="addressFormModalEmail" name="addressFormModalEmail" type="text" maxlength="40"
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: EMAIL*
+ parseable name: addressFormModalEmail
+ field signature: 1977954575
+ form signature: 8397269939060577503"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ <div id="addressNickname">
+ <label for="addressFormModalAddressNickName">ADDRESS NICKNAME<span>*</span>
+ <span>&nbsp;<span>?</span>
+</span>
+ <span>The Address Nickname is a short name you create to help you easily identify this address within your address book.</span>
+ </label>
+ <input id="addressFormModalAddressNickName" name="addressFormModalAddressNickName" type="text" maxlength="35" placeholder="Holly at school, Mom, etc."
+title="overall type: ADDRESS_HOME_STREET_ADDRESS
+ server type: ADDRESS_HOME_STREET_ADDRESS
+ heuristic type: UNKNOWN_TYPE
+ label: ADDRESS NICKNAME* The Address Nickname is a short name you create to help you easily identify this a
+ parseable name: addressFormModalAddressNickName
+ field signature: 605446661
+ form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_STREET_ADDRESS"
+>
+ </div>
+ <div>
+ <input name="saveAddressCheckbox" id="saveAddressCheckbox" type="checkbox"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Add to address book.
+ parseable name: saveAddressCheckbox
+ field signature: 784127875
+ form signature: 8397269939060577503"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="saveAddressCheckbox">Add to address book. </label>
+ </div>
+ <div>
+ <input name="setDefaultCheckbox" id="setDefaultCheckbox" type="checkbox" disabled="true"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save as default shipping address in Address Book
+ parseable name: setDefaultCheckbox
+ field signature: 1479095059
+ form signature: 8397269939060577503"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="setDefaultCheckbox" id="setDefaultCheckboxModalLabel">Save as default billing address in Address Book</label>
+ </div>
+ </div>
+ </form>
+ <form id="AddressFormInline-Form" name="AddressFormInline-Form" autocomplete="on" method="post">
+ <div>
+ <input type="hidden" name="addressType" value="B">
+ <p id="addressFormInlineRequired" tabindex="-1">
+<span>*</span> Required fields</p>
+ <div id="personName">
+ <div>
+ <label for="addressFormInlineFirstName">FIRST NAME<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineFirstName" name="addressFormInlineFirstName"
+title="overall type: NAME_FIRST
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: FIRST NAME*
+ parseable name: addressFormInlineFirstName
+ field signature: 3938958812
+ form signature: 16870043504464996221" type="text" maxlength="40"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineMiddleInitial">M.I.</label>
+ <div>
+ <input id="addressFormInlineMiddleInitial" name="addressFormInlineMiddleInitial"
+title="overall type: NAME_MIDDLE_INITIAL
+ server type: NAME_MIDDLE_INITIAL
+ heuristic type: NAME_MIDDLE_INITIAL
+ label: M.I.
+ parseable name: addressFormInlineMiddleInitial
+ field signature: 3429701181
+ form signature: 16870043504464996221" type="text" maxlength="1"
+autofill-prediction="NAME_MIDDLE_INITIAL"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineLastName">LAST NAME<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineLastName" name="addressFormInlineLastName"
+title="overall type: NAME_LAST
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: LAST NAME*
+ parseable name: addressFormInlineLastName
+ field signature: 2108416564
+ form signature: 16870043504464996221" type="text" maxlength="40"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ </div>
+ </div>
+ <div id="city">
+ <label for="addressFormInlineCompany">COMPANY NAME</label>
+ <div>
+ <input id="addressFormInlineCompany" name="addressFormInlineCompany" type="text" maxlength="40"
+title="overall type: COMPANY_NAME
+ server type: COMPANY_NAME
+ heuristic type: COMPANY_NAME
+ label: COMPANY NAME
+ parseable name: addressFormInlineCompany
+ field signature: 4087238350
+ form signature: 16870043504464996221"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineCountry">COUNTRY<span>*</span>
+</label>
+ <div>
+ <select id="addressFormInlineCountry" name="addressFormInlineCountry"
+title="overall type: ADDRESS_HOME_COUNTRY
+ server type: ADDRESS_HOME_COUNTRY
+ heuristic type: ADDRESS_HOME_COUNTRY
+ label: COUNTRY*
+ parseable name: addressFormInlineCountry
+ field signature: 695762362
+ form signature: 16870043504464996221"
+autofill-prediction="ADDRESS_HOME_COUNTRY"
+>
+ <option value="CA">Canada</option>
+ <option value="US">United States</option>
+ </select>
+ </div>
+ </div>
+ <div id="streetAddress">
+ <legend>
+ <label for="addressFormInlineAddressLine1">STREET ADDRESS<span>*</span>
+</label>
+ </legend>
+ <div>
+ <input id="addressFormInlineAddressLine1" name="addressFormInlineAddressLine1" placeholder="Address Line 1" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: STREET ADDRESS*
+ parseable name: addressFormInlineAddressLine1
+ field signature: 1040409778
+ form signature: 16870043504464996221"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <input id="addressFormInlineAddressLine2" name="addressFormInlineAddressLine2" placeholder="Address Line 2" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address Line 2
+ parseable name: addressFormInlineAddressLine2
+ field signature: 1640842807
+ form signature: 16870043504464996221"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ <div id="city">
+ <label for="addressFormInlineCity">CITY<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineCity" name="addressFormInlineCity" type="text" maxlength="40"
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: CITY*
+ parseable name: addressFormInlineCity
+ field signature: 2829321141
+ form signature: 16870043504464996221"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ </div>
+ <div id="state">
+ <div>
+ <label for="addressFormInlineState">STATE / PROVINCE<span>*</span>
+</label>
+ <div>
+ <select id="addressFormInlineState" name="addressFormInlineState"
+title="overall type: ADDRESS_HOME_STATE
+ server type: ADDRESS_HOME_STATE
+ heuristic type: ADDRESS_HOME_STATE
+ label: STATE / PROVINCE*
+ parseable name: addressFormInlineState
+ field signature: 3295167441
+ form signature: 16870043504464996221"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="NO_STATE_TYPE_SELECTED" selected="selected">* Select</option>
+ <option value="Aa">AA - Armed Forces America</option>
+ <option value="Ae">AE - Armed Forces Europe</option>
+ <option value="AL">Alabama</option>
+ <option value="AK">Alaska</option>
+ <option value="Ap">AP - Armed Forces Pacific</option>
+ <option value="AZ">Arizona</option>
+ <option value="AR">Arkansas</option>
+ <option value="CA">California</option>
+ <option value="CO">Colorado</option>
+ <option value="CT">Connecticut</option>
+ <option value="DE">Delaware</option>
+ <option value="DC">District of Columbia</option>
+ <option value="FL">Florida</option>
+ <option value="GA">Georgia</option>
+ <option value="HI">Hawaii</option>
+ <option value="ID">Idaho</option>
+ <option value="IL">Illinois</option>
+ <option value="IN">Indiana</option>
+ <option value="IA">Iowa</option>
+ <option value="KS">Kansas</option>
+ <option value="KY">Kentucky</option>
+ <option value="LA">Louisiana</option>
+ <option value="ME">Maine</option>
+ <option value="MD">Maryland</option>
+ <option value="MA">Massachusetts</option>
+ <option value="MI">Michigan</option>
+ <option value="MN">Minnesota</option>
+ <option value="MS">Mississippi</option>
+ <option value="MO">Missouri</option>
+ <option value="MT">Montana</option>
+ <option value="NE">Nebraska</option>
+ <option value="NV">Nevada</option>
+ <option value="NH">New Hampshire</option>
+ <option value="NJ">New Jersey</option>
+ <option value="NM">New Mexico</option>
+ <option value="NY">New York</option>
+ <option value="NC">North Carolina</option>
+ <option value="ND">North Dakota</option>
+ <option value="OH">Ohio</option>
+ <option value="OK">Oklahoma</option>
+ <option value="OR">Oregon</option>
+ <option value="PA">Pennsylvania</option>
+ <option value="PR">Puerto Rico</option>
+ <option value="RI">Rhode Island</option>
+ <option value="SC">South Carolina</option>
+ <option value="SD">South Dakota</option>
+ <option value="TN">Tennessee</option>
+ <option value="TX">Texas</option>
+ <option value="UT">Utah</option>
+ <option value="VT">Vermont</option>
+ <option value="VA">Virginia</option>
+ <option value="WA">Washington</option>
+ <option value="WV">West Virginia</option>
+ <option value="WI">Wisconsin</option>
+ <option value="WY">Wyoming</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineZip">ZIP / POSTAL CODE<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineZip" name="addressFormInlineZip" type="text" maxlength="10"
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP / POSTAL CODE*
+ parseable name: addressFormInlineZip
+ field signature: 3060672026
+ form signature: 16870043504464996221"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </div>
+ </div>
+ <div id="phoneNumber">
+ <label for="addressFormInlinePhoneNumber">PHONE NUMBER<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlinePhoneNumber" name="addressFormInlinePhoneNumber" type="text" maxlength="32"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER
+ server type: PHONE_HOME_CITY_AND_NUMBER
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: PHONE NUMBER*
+ parseable name: addressFormInlinePhoneNumber
+ field signature: 1198968276
+ form signature: 16870043504464996221"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </div>
+ </div>
+ <div id="email">
+ <label for="addressFormInlineEmail" id="addressFormInlineEmailLabel">EMAIL<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineEmail" name="addressFormInlineEmail" type="text" maxlength="40" value=""
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: EMAIL*
+ parseable name: addressFormInlineEmail
+ field signature: 2460631353
+ form signature: 16870043504464996221"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ </div>
+ <div id="addressNickname">
+ <label for="addressFormInlineAddressNickName">ADDRESS NICKNAME<span>*</span>
+<span>&nbsp;<span>?</span>
+</span>
+<span>The Address Nickname is a short name you create to help you easily identify this address within your address book.</span>
+</label>
+ <div>
+ <input id="addressFormInlineAddressNickName" name="addressFormInlineAddressNickName" type="text" maxlength="35" placeholder="Holly at school, Mom, etc."
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: ADDRESS NICKNAME* The Address Nickname is a short name you create to help you easily identify this a
+ parseable name: addressFormInlineAddressNickName
+ field signature: 2948011243
+ form signature: 16870043504464996221"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ <div>
+ <input name="saveAddressCheckboxInline" id="saveAddressCheckboxInline" type="checkbox"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Add to address book.
+ parseable name: saveAddressCheckboxInline
+ field signature: 3323717546
+ form signature: 16870043504464996221"
+autofill-prediction="UNKNOWN_TYPE" checked="checked"
+>
+ <label for="saveAddressCheckboxInline">Add to address book. </label>
+ </div>
+ <div>
+ <input name="setDefaultCheckboxInline" id="setDefaultCheckboxInline" type="checkbox"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save as default shipping address in Address Book
+ parseable name: setDefaultCheckboxInline
+ field signature: 2923970107
+ form signature: 16870043504464996221"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="setDefaultCheckboxInline" id="setDefaultCheckboxInlineLabel">Save as default billing address in Address Book</label>
+ </div>
+ <div id="defaultAddressChangeInline">
+ <div id="WC_ContentAreaESpot_div_1_rx-DefaultAddrConfirm">
+ <div id="WC_ContentAreaESpot_div_2_rx-DefaultAddrConfirm">[rx-DefaultAddrConfirm]</div>
+ <div>
+ <ul>
+ <li value="1">
+ You are changing your Costco Default Shipping Address. &nbsp;All future orders from Costco.com, including Pharmacy Prescription Orders, will be sent to this Address.
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div id="button-container">
+ <div>
+ <div>
+ <button id="addressFormInlineButton" type="button">
+<span>
+<span>Save Address</span>
+</span>
+</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ <div id="footer-find-warehouse-block">
+ <label for="footer-search-field">Find a Warehouse</label>
+ <form id="WarehouseSearchForm" action="https://www.costco.com/warehouse-locations" novalidate="novalidate">
+ <div>
+ <input id="footer-search-field" type="search" name="location" tabindex="1" placeholder="City, state or zip" value=""
+title="Search">
+ <input type="submit" id="searchClear" value="Clear">
+ <input type="hidden" id="fromWLocSubmit" name="fromWLocSubmit" value="true">
+ <input type="hidden" id="numOfWarehouses" name="numOfWarehouses" value="10">
+ </div>
+ </form>
+ </div>
+ <div id="footer-email-offers-block">
+ <label for="footer-email-offers">Get Email Offers</label>
+ <form
+title="" action="https://www.costco.com/EmailSubscription" id="EmailOffersForm">
+ <div>
+ <input type="text" name="emailSignUp" id="footer-email-offers" placeholder="Enter your email">
+ <span>
+ <button type="submit" alt="Go">Go</button>
+ </span>
+ </div>
+ </form>
+ </div>
+ <input type="hidden" name="typeAheadDisabled" id="typeAheadDisabled" value="false">
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta feature="9.1.2">
+ <title>Shipping</title>
+ <meta name="currentBuildNumber" content="3.0.29057.0">
+ <meta name="ServerName" content="www.costco.com">
+ <meta name="LocalAddress" content="xxx.xxx.xxx.48">
+ <meta name="LocalName" content="TP26">
+ </head>
+ <body>
+ <form name="ShipAsCompleteForm" method="post" action="https://www.costco.com/CostcoMultiShippingCmd" id="ShipAsCompleteForm">
+ <input type="hidden" name="storeId" value="10301">
+ <input type="hidden" name="langId" value="-1">
+ <input type="hidden" name="orderId" value="644156669">
+ <input type="hidden" name="catalogId" value="10701">
+ <input type="hidden" name="multiAddressShipping" value="false">
+ <input type="hidden" name="URL" value="CheckoutPaymentView">
+ </form>
+ <form name="NewAddressSingleShippingForm" method="post" action="https://www.costco.com/CostcoSelectShippingCmd" id="NewAddressSingleShippingForm">
+ <input type="hidden" name="storeId" value="10301">
+ <input type="hidden" name="langId" value="-1">
+ <input type="hidden" name="catalogId" value="10701">
+ <input type="hidden" name="action" value="SingleShipping">
+ <input type="hidden" name="addressId" value="">
+ <input type="hidden" name="authToken" value="">
+ </form>
+ <form id="AddressFormModal-Form" name="AddressFormModal-Form" autocomplete="on" method="post">
+ <div>
+ <input type="hidden" name="addressType" value="S">
+ <p id="addressFormModalRequired" tabindex="-1">
+ <span>*</span> Required fields</p>
+ <div id="personName">
+ <div>
+<label for="addressFormModalFirstName">FIRST NAME<span>*</span>
+</label>
+ <input id="addressFormModalFirstName" name="addressFormModalFirstName"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: FIRST NAME* parseable name: addressFormModalFirstName field signature: 2222266781 form signature: 8397269939060577503" type="text" maxlength="40"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ <div>
+<label for="addressFormModalMiddleInitial">M.I.</label>
+ <input id="addressFormModalMiddleInitial" name="addressFormModalMiddleInitial"
+title="overall type: NAME_MIDDLE_INITIAL server type: NAME_MIDDLE_INITIAL heuristic type: NAME_MIDDLE_INITIAL label: M.I. parseable name: addressFormModalMiddleInitial field signature: 3540652809 form signature: 8397269939060577503" type="text" maxlength="1"
+autofill-prediction="NAME_MIDDLE_INITIAL"
+>
+ </div>
+ <div>
+<label for="addressFormModalLastName">LAST NAME<span>*</span>
+</label>
+ <input id="addressFormModalLastName" name="addressFormModalLastName"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: LAST NAME* parseable name: addressFormModalLastName field signature: 4218996568 form signature: 8397269939060577503" type="text" maxlength="40"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ </div>
+ <div>
+<label for="addressFormModalCompany">COMPANY NAME</label>
+ <input id="addressFormModalCompany" name="addressFormModalCompany" type="text" maxlength="40"
+title="overall type: COMPANY_NAME server type: COMPANY_NAME heuristic type: COMPANY_NAME label: COMPANY NAME parseable name: addressFormModalCompany field signature: 1845178698 form signature: 8397269939060577503"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ <div>
+ <label for="addressFormModalCountry">COUNTRY<span>*</span>
+</label>
+ <select id="addressFormModalCountry" name="addressFormModalCountry"
+title="overall type: ADDRESS_HOME_COUNTRY server type: ADDRESS_HOME_COUNTRY heuristic type: ADDRESS_HOME_COUNTRY label: COUNTRY* parseable name: addressFormModalCountry field signature: 4052501735 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_COUNTRY"
+>
+ <option value="US">United States</option>
+ </select>
+ </div>
+ <div id="streetAddress">
+ <legend>
+<label for="addressFormModalAddressLine1">STREET ADDRESS<span>*</span>
+</label>
+</legend>
+ <div>
+ <input id="addressFormModalAddressLine1" name="addressFormModalAddressLine1" placeholder="Address Line 1" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: STREET ADDRESS* parseable name: addressFormModalAddressLine1 field signature: 1532865404 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <div>
+ <input id="addressFormModalAddressLine2" name="addressFormModalAddressLine2" placeholder="Address Line 2" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Address Line 2 parseable name: addressFormModalAddressLine2 field signature: 2315514959 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ </div>
+ <div id="city">
+<label for="addressFormModalCity">CITY<span>*</span>
+</label>
+ <input id="addressFormModalCity" name="addressFormModalCity" type="text" maxlength="40"
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: CITY* parseable name: addressFormModalCity field signature: 4130865920 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ <div id="stateAndZip">
+ <div>
+ <label for="addressFormModalState">STATE / PROVINCE<span>*</span>
+</label>
+ <select id="addressFormModalState" name="addressFormModalState"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: STATE / PROVINCE* parseable name: addressFormModalState field signature: 4026908515 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="NO_STATE_TYPE_SELECTED" selected="selected">* Select</option>
+ <option value="Aa">AA - Armed Forces America</option>
+ <option value="Ae">AE - Armed Forces Europe</option>
+ <option value="AL"> Alabama</option>
+ <option value="AK"> Alaska</option>
+ <option value="Ap">AP - Armed Forces Pacific</option>
+ <option value="AZ"> Arizona</option>
+ <option value="AR"> Arkansas</option>
+ <option value="CA"> California</option>
+ <option value="CO"> Colorado</option>
+ <option value="CT"> Connecticut</option>
+ <option value="DE"> Delaware</option>
+ <option value="DC"> District of Columbia</option>
+ <option value="FL"> Florida</option>
+ <option value="GA"> Georgia</option>
+ <option value="HI"> Hawaii</option>
+ <option value="ID"> Idaho</option>
+ <option value="IL"> Illinois</option>
+ <option value="IN"> Indiana</option>
+ <option value="IA"> Iowa</option>
+ <option value="KS"> Kansas</option>
+ <option value="KY"> Kentucky</option>
+ <option value="LA"> Louisiana</option>
+ <option value="ME"> Maine</option>
+ <option value="MD"> Maryland</option>
+ <option value="MA"> Massachusetts</option>
+ <option value="MI"> Michigan</option>
+ <option value="MN"> Minnesota</option>
+ <option value="MS"> Mississippi</option>
+ <option value="MO"> Missouri</option>
+ <option value="MT"> Montana</option>
+ <option value="NE"> Nebraska</option>
+ <option value="NV"> Nevada</option>
+ <option value="NH">New Hampshire</option>
+ <option value="NJ">New Jersey</option>
+ <option value="NM">New Mexico</option>
+ <option value="NY">New York</option>
+ <option value="NC">North Carolina</option>
+ <option value="ND">North Dakota</option>
+ <option value="OH"> Ohio</option>
+ <option value="OK"> Oklahoma</option>
+ <option value="OR"> Oregon</option>
+ <option value="PA"> Pennsylvania</option>
+ <option value="PR">Puerto Rico</option>
+ <option value="RI">Rhode Island</option>
+ <option value="SC">South Carolina</option>
+ <option value="SD">South Dakota</option>
+ <option value="TN"> Tennessee</option>
+ <option value="TX"> Texas</option>
+ <option value="UT"> Utah</option>
+ <option value="VT"> Vermont</option>
+ <option value="VA"> Virginia</option>
+ <option value="WA"> Washington</option>
+ <option value="WV">West Virginia</option>
+ <option value="WI"> Wisconsin</option>
+ <option value="WY"> Wyoming</option>
+ </select>
+ </div>
+ <div>
+<label for="addressFormModalZip">ZIP / POSTAL CODE<span>*</span>
+</label>
+ <input id="addressFormModalZip" name="addressFormModalZip" type="text" maxlength="10"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: ZIP / POSTAL CODE* parseable name: addressFormModalZip field signature: 2383002781 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </div>
+ <div id="phoneNumber">
+<label for="addressFormModalPhoneNumber">PHONE NUMBER<span>*</span>
+</label>
+ <input id="addressFormModalPhoneNumber" name="addressFormModalPhoneNumber" type="text" maxlength="32"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: PHONE NUMBER* parseable name: addressFormModalPhoneNumber field signature: 1884423068 form signature: 8397269939060577503"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </div>
+ <div id="email">
+<label for="addressFormModalEmail" id="addressFormModalEmailLabel">EMAIL<span>*</span>
+</label>
+ <input id="addressFormModalEmail" name="addressFormModalEmail" type="text" maxlength="40"
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: EMAIL* parseable name: addressFormModalEmail field signature: 1977954575 form signature: 8397269939060577503"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ <div id="addressNickname">
+<label for="addressFormModalAddressNickName">ADDRESS NICKNAME<span>*</span>
+<span>&nbsp;<span>?</span>
+</span>
+<span>The Address Nickname is a short name you create to help you easily identify this address within your address book.</span>
+</label>
+ <input id="addressFormModalAddressNickName" name="addressFormModalAddressNickName" type="text" maxlength="35" placeholder="Holly at school, Mom, etc."
+title="overall type: ADDRESS_HOME_STREET_ADDRESS server type: ADDRESS_HOME_STREET_ADDRESS heuristic type: UNKNOWN_TYPE label: ADDRESS NICKNAME* The Address Nickname is a short name you create to help you easily identify this a parseable name: addressFormModalAddressNickName field signature: 605446661 form signature: 8397269939060577503"
+autofill-prediction="ADDRESS_HOME_STREET_ADDRESS"
+>
+ </div>
+ <div>
+ <input name="saveAddressCheckbox" id="saveAddressCheckbox" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Add to address book. parseable name: saveAddressCheckbox field signature: 784127875 form signature: 8397269939060577503"
+autofill-prediction="UNKNOWN_TYPE"
+>
+<label for="saveAddressCheckbox">Add to address book.</label>
+ </div>
+ <div>
+ <input name="setDefaultCheckbox" id="setDefaultCheckbox" type="checkbox" disabled="true"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Save as default shipping address in Address Book parseable name: setDefaultCheckbox field signature: 1479095059 form signature: 8397269939060577503"
+autofill-prediction="UNKNOWN_TYPE"
+>
+<label for="setDefaultCheckbox" id="setDefaultCheckboxModalLabel">Save as default shipping address in Address Book</label>
+ </div>
+ </div>
+ </form>
+ <form id="AddressFormInline-Form" name="AddressFormInline-Form" autocomplete="on" method="post">
+ <div>
+ <input type="hidden" name="addressType" value="S">
+ <p id="addressFormInlineRequired" tabindex="-1">
+<span>*</span> Required fields</p>
+ <div id="personName">
+ <div>
+ <label for="addressFormInlineFirstName">FIRST NAME<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineFirstName" name="addressFormInlineFirstName"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: FIRST NAME* parseable name: addressFormInlineFirstName field signature: 3938958812 form signature: 9207149805122018522" type="text" maxlength="40"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineMiddleInitial">M.I.</label>
+ <div>
+ <input id="addressFormInlineMiddleInitial" name="addressFormInlineMiddleInitial"
+title="overall type: NAME_MIDDLE_INITIAL server type: NAME_MIDDLE_INITIAL heuristic type: NAME_MIDDLE_INITIAL label: M.I. parseable name: addressFormInlineMiddleInitial field signature: 3429701181 form signature: 9207149805122018522" type="text" maxlength="1"
+autofill-prediction="NAME_MIDDLE_INITIAL"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineLastName">LAST NAME<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineLastName" name="addressFormInlineLastName"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: LAST NAME* parseable name: addressFormInlineLastName field signature: 2108416564 form signature: 9207149805122018522" type="text" maxlength="40"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ </div>
+ </div>
+ <div id="city">
+ <label for="addressFormInlineCompany">COMPANY NAME</label>
+ <div>
+ <input id="addressFormInlineCompany" name="addressFormInlineCompany" type="text" maxlength="40"
+title="overall type: COMPANY_NAME server type: COMPANY_NAME heuristic type: COMPANY_NAME label: COMPANY NAME parseable name: addressFormInlineCompany field signature: 4087238350 form signature: 9207149805122018522"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineCountry">COUNTRY<span>*</span>
+</label>
+ <div>
+ <select id="addressFormInlineCountry" name="addressFormInlineCountry"
+title="overall type: ADDRESS_HOME_COUNTRY server type: ADDRESS_HOME_COUNTRY heuristic type: ADDRESS_HOME_COUNTRY label: COUNTRY* parseable name: addressFormInlineCountry field signature: 695762362 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_COUNTRY"
+>
+ <option value="US">United States</option>
+ </select>
+ </div>
+ </div>
+ <div id="streetAddress">
+ <legend>
+<label for="addressFormInlineAddressLine1">STREET ADDRESS<span>*</span>
+</label>
+ </legend>
+ <div>
+ <input id="addressFormInlineAddressLine1" name="addressFormInlineAddressLine1" placeholder="Address Line 1" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: STREET ADDRESS* parseable name: addressFormInlineAddressLine1 field signature: 1040409778 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <input id="addressFormInlineAddressLine2" name="addressFormInlineAddressLine2" placeholder="Address Line 2" type="text" maxlength="30"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Address Line 2 parseable name: addressFormInlineAddressLine2 field signature: 1640842807 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ <div id="city">
+ <label for="addressFormInlineCity">CITY<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineCity" name="addressFormInlineCity" type="text" maxlength="40"
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: CITY* parseable name: addressFormInlineCity field signature: 2829321141 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ </div>
+ <div id="state">
+ <div>
+ <label for="addressFormInlineState">STATE / PROVINCE<span>*</span>
+</label>
+ <div>
+ <select id="addressFormInlineState" name="addressFormInlineState"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: STATE / PROVINCE* parseable name: addressFormInlineState field signature: 3295167441 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="NO_STATE_TYPE_SELECTED" selected="selected">* Select</option>
+ <option value="Aa">AA - Armed Forces America</option>
+ <option value="Ae">AE - Armed Forces Europe</option>
+ <option value="AL"> Alabama</option>
+ <option value="AK"> Alaska</option>
+ <option value="Ap">AP - Armed Forces Pacific</option>
+ <option value="AZ"> Arizona</option>
+ <option value="AR"> Arkansas</option>
+ <option value="CA"> California</option>
+ <option value="CO"> Colorado</option>
+ <option value="CT"> Connecticut</option>
+ <option value="DE"> Delaware</option>
+ <option value="DC"> District of Columbia</option>
+ <option value="FL"> Florida</option>
+ <option value="GA"> Georgia</option>
+ <option value="HI"> Hawaii</option>
+ <option value="ID"> Idaho</option>
+ <option value="IL"> Illinois</option>
+ <option value="IN"> Indiana</option>
+ <option value="IA"> Iowa</option>
+ <option value="KS"> Kansas</option>
+ <option value="KY"> Kentucky</option>
+ <option value="LA"> Louisiana</option>
+ <option value="ME"> Maine</option>
+ <option value="MD"> Maryland</option>
+ <option value="MA"> Massachusetts</option>
+ <option value="MI"> Michigan</option>
+ <option value="MN"> Minnesota</option>
+ <option value="MS"> Mississippi</option>
+ <option value="MO"> Missouri</option>
+ <option value="MT"> Montana</option>
+ <option value="NE"> Nebraska</option>
+ <option value="NV"> Nevada</option>
+ <option value="NH">New Hampshire</option>
+ <option value="NJ">New Jersey</option>
+ <option value="NM">New Mexico</option>
+ <option value="NY">New York</option>
+ <option value="NC">North Carolina</option>
+ <option value="ND">North Dakota</option>
+ <option value="OH"> Ohio</option>
+ <option value="OK"> Oklahoma</option>
+ <option value="OR"> Oregon</option>
+ <option value="PA"> Pennsylvania</option>
+ <option value="PR">Puerto Rico</option>
+ <option value="RI">Rhode Island</option>
+ <option value="SC">South Carolina</option>
+ <option value="SD">South Dakota</option>
+ <option value="TN"> Tennessee</option>
+ <option value="TX"> Texas</option>
+ <option value="UT"> Utah</option>
+ <option value="VT"> Vermont</option>
+ <option value="VA"> Virginia</option>
+ <option value="WA"> Washington</option>
+ <option value="WV">West Virginia</option>
+ <option value="WI"> Wisconsin</option>
+ <option value="WY"> Wyoming</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label for="addressFormInlineZip">ZIP / POSTAL CODE<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineZip" name="addressFormInlineZip" type="text" maxlength="10"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: ZIP / POSTAL CODE* parseable name: addressFormInlineZip field signature: 3060672026 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </div>
+ </div>
+ <div id="phoneNumber">
+ <label for="addressFormInlinePhoneNumber">PHONE NUMBER<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlinePhoneNumber" name="addressFormInlinePhoneNumber" type="text" maxlength="32"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: PHONE NUMBER* parseable name: addressFormInlinePhoneNumber field signature: 1198968276 form signature: 9207149805122018522"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </div>
+ </div>
+ <div id="email">
+ <label for="addressFormInlineEmail" id="addressFormInlineEmailLabel">EMAIL<span>*</span>
+</label>
+ <div>
+ <input id="addressFormInlineEmail" name="addressFormInlineEmail" type="text" maxlength="40" value=""
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: EMAIL* parseable name: addressFormInlineEmail field signature: 2460631353 form signature: 9207149805122018522"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ </div>
+ <div id="addressNickname">
+ <label for="addressFormInlineAddressNickName">ADDRESS NICKNAME<span>*</span>
+<span>&nbsp;<span>?</span>
+</span>
+<span>The Address Nickname is a short name you create to help you easily identify this address within your address book.</span>
+</label>
+ <div>
+ <input id="addressFormInlineAddressNickName" name="addressFormInlineAddressNickName" type="text" maxlength="35" placeholder="Holly at school, Mom, etc."
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: ADDRESS NICKNAME* ?The Address Nickname is a short name you create to help you easily identify this parseable name: addressFormInlineAddressNickName field signature: 2948011243 form signature: 9207149805122018522"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ <div>
+ <input name="saveAddressCheckboxInline" id="saveAddressCheckboxInline" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Add to address book. parseable name: saveAddressCheckboxInline field signature: 3323717546 form signature: 9207149805122018522"
+autofill-prediction="UNKNOWN_TYPE" checked="checked"
+>
+<label for="saveAddressCheckboxInline">Add to address book.</label>
+ </div>
+ <div>
+ <input name="setDefaultCheckboxInline" id="setDefaultCheckboxInline" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Save as default shipping address in Address Book parseable name: setDefaultCheckboxInline field signature: 2923970107 form signature: 9207149805122018522"
+autofill-prediction="UNKNOWN_TYPE"
+>
+<label for="setDefaultCheckboxInline" id="setDefaultCheckboxInlineLabel">Save as default shipping address in Address Book</label>
+ </div>
+ <div id="defaultAddressChangeInline">
+ <div id="WC_ContentAreaESpot_div_1_rx-DefaultAddrConfirm">
+ <div id="WC_ContentAreaESpot_div_2_rx-DefaultAddrConfirm">
+ [rx-DefaultAddrConfirm]
+ </div>
+ <div>
+ <ul>
+ <li value="1"> You are changing your Costco Default Shipping Address. &nbsp;All future orders from Costco.com, including Pharmacy Prescription Orders, will be sent to this Address.
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input name="copyShippingCheckboxInline" id="copyShippingCheckboxInline" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Use as my default billing address parseable name: copyShippingCheckboxInline field signature: 1184640612 form signature: 9207149805122018522"
+autofill-prediction="UNKNOWN_TYPE"
+>
+<label for="copyShippingCheckboxInline" id="copyShippingCheckboxInlineLabel">Use as my default billing address</label>
+ </div>
+ <div id="billingNicknameDiv">
+ <label>Billing Address Nickname
+ <span>*</span>
+<span>&nbsp;<span>?</span>
+</span>
+<span>The Address Nickname is a short name you create to help you easily identify this address within your address book.</span>
+</label>
+ <input type="text" id="billingNickname" name="billingNickname" maxlength="40" value="New Billing"
+title="overall type: ADDRESS_HOME_STREET_ADDRESS server type: ADDRESS_HOME_STREET_ADDRESS heuristic type: UNKNOWN_TYPE label: Billing Address Nickname * ?The Address Nickname is a short name you create to help you easily ident parseable name: billingNickname field signature: 4200328961 form signature: 9207149805122018522"
+autofill-prediction="ADDRESS_HOME_STREET_ADDRESS"
+>
+ </div>
+ <div id="button-container">
+ <div>
+ <div>
+<button id="addressFormInlineButton" type="button">
+ <span>
+<span>Ship to this Address</span>
+</span>
+</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ <div id="footer-find-warehouse-block">
+ <label for="footer-search-field" >Find a Warehouse</label>
+ <form id="WarehouseSearchForm" action="https://www.costco.com/warehouse-locations" novalidate="novalidate" name="WarehouseSearchForm">
+ <div>
+ <input id="footer-search-field" type="search" name="location" tabindex="1" placeholder="City, state or zip" value=""
+title="Search">
+ <input type="submit" id="searchClear" value="Clear">
+ <input type="hidden" id="fromWLocSubmit" name="fromWLocSubmit" value="true">
+ <input type="hidden" id="numOfWarehouses" name="numOfWarehouses" value="10">
+ </div>
+ </form>
+ </div>
+ <div id="footer-email-offers-block">
+ <label for="footer-email-offers">Get Email Offers</label>
+ <form
+title="" action="https://www.costco.com/EmailSubscription" id="EmailOffersForm" name="EmailOffersForm">
+ <div>
+ <input type="text" name="emailSignUp" id="footer-email-offers" placeholder="Enter your email">
+<span>
+<button type="submit" alt="Go">
+<span>
+<span>Go</span>
+</span>
+</button>
+</span>
+ </div>
+ </form>
+ </div>
+ <div>
+ <label>Follow Us</label>
+ <ul>
+ <li>
+<a>
+<label>facebook</label>
+</a>
+</li>
+ <li>
+<a>
+<label>pinterest</label>
+</a>
+</li>
+ </ul>
+ </div>
+ <form name="SingleShippingForm" method="post" action="https://www.costco.com/CostcoSelectShippingCmd" id="SingleShippingForm">
+ <input type="hidden" name="storeId" value="10301">
+ <input type="hidden" name="langId" value="-1">
+ <input type="hidden" name="catalogId" value="10701">
+ <input type="hidden" name="action" value="SingleShipping">
+ <input type="hidden" name="addressId" value="">
+ <input type="hidden" name="authToken" value="">
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Sign In</title>
+ <meta name="currentBuildNumber" content="3.0.29057.0">
+ <meta name="ServerName" content="www.costco.com">
+ <meta name="LocalAddress" content="xxx.xxx.xxx.48">
+ <meta name="LocalName" content="TP26">
+ </head>
+ <body waid71fa0d88-5390-4b5b-a2f4-e45fa93d85e2="SA password protect entry checker">
+ <form action="https://www.costco.com/CatalogSearch">
+ <input type="submit" value="Submit">
+<label for="search-field">Search</label>
+ <div>
+ <label>Search Icon</label>
+<span style=/"position: relative; display: inline-block;">
+ <input type="text" tabindex="-1"
+title="Search" readonly autocomplete="off" spellcheck="false" dir="ltr">
+ <input id="search-field" type="text" name="keyword" tabindex="1" placeholder="Search Costco"
+title="Search" autocomplete="off" spellcheck="false" dir="auto" >
+ </span>
+ <div>
+ <div>
+</div>
+ </div>
+ </div>
+ <input type="submit" value="Submit" tabindex="-1">
+ </form>
+ <form id="warehouse_locator_search" action='https://www.costco.com/warehouse-locations'>
+ <div>
+ <input id="warehouse-search-field"
+title="Warehouse Search Field" name="location" type="search" value=""/>
+ </div>
+ <input type="hidden" name="tiresCheckout" value="" />
+ <input type="hidden" name="orderitemId" value="" />
+ <input type="hidden" name="storeId" value="10301" />
+ <input type="hidden" name="catalogId" value="10701" />
+ <input type="hidden" name="fromPage" value="" />
+ <label for="locator_search_filters">Show Warehouses with:</label>
+ <div id="locator_search_filters">
+ <div>
+ <div>
+ <input id="hasGas" type="checkbox" name="hasGas" value="true"
+title="Gas Station" />
+ <label for="hasGas"
+title="Gas Station">
+<i>
+</i>
+ <span>Gas Station</span>
+ </label>
+ </div>
+ <div>
+ <input id="hasTires" type="checkbox" name="hasTires" value="true"
+title="Tire Service" />
+ <label for="hasTires"
+title="Tire Service">
+<i>
+</i>
+ <span>Tire Center</span>
+ </label>
+ </div>
+ <div>
+ <input id="hasFood" type="checkbox" name="hasFood" value="true"
+title="Food Court" />
+ <label for="hasFood"
+title="Food Court">
+<i>
+</i>
+ <span>Food Court</span>
+ </label>
+ </div>
+ <div>
+ <input id="hasHearing" type="checkbox" name="hasHearing" value="true"
+title="Hearing Aids" />
+ <label for="hasHearing"
+title="Hearing Aids">
+<i>
+</i>
+ <span>Hearing Aids</span>
+ </label>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input id="hasOptical" type="checkbox" name="hasOptical" value="true"
+title="Optical Dept" />
+ <label for="hasOptical"
+title="Optical Dept">
+<i>
+</i>
+ <span>Optical</span>
+ </label>
+ </div>
+ <div>
+ <input id="hasPharmacy" type="checkbox" name="hasPharmacy" value="true"
+title="Pharmacy" />
+ <label for="hasPharmacy"
+title="Pharmacy">
+<i>
+</i>
+ <span>Pharmacy</span>
+ </label>
+ </div>
+ <div>
+ <input id="hasBusiness" type="checkbox" name="hasBusiness" value="true"
+title="Business" />
+ <label for="hasBusiness"
+title="Business">
+<i>
+</i>
+ <span>Business Center</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <input type="hidden" id="fromWLocSubmit" name="fromWLocSubmit" value="true" />
+ <input type="hidden" id="numOfWarehouses" name="numOfWarehouses" value="10" />
+ <input type="submit" value="Find a Warehouse"/>
+ </form>
+ <div id="email-offer-popover-container">
+ <label for="header_emailSignUpEmail">Get Email Offers</label>
+ <label>Sign up for great offers from Costco.com!</label>
+ <form
+title="" action="/EmailSubscription" id="header_emailSignup">
+ <div>
+ <input id="header_emailSignUpEmail" type="text" name="emailSignUp" placeholder="Enter your email">
+ <span>
+ <button type="submit" alt="Go">Go</button>
+ </span>
+ </div>
+ </form>
+ </div>
+ <form action="https://www.costco.com/Logoff?URL=TopCategoriesDisplay">
+ <li>
+ <input type="submit" value="Sign Out"/>
+ </li>
+ </form>
+ <form action="/EmailSubscription">
+ <div>
+ <label for="modal_email_offers">Sign up for great offers from Costco.com!</label>
+ <input type="text" id="modal_email_offers" name="emailSignUp" placeholder="Enter your email"/>
+ </div>
+ </form>
+ <form
+title="" name="LogonForm" method="post" action="https://www.costco.com/Logon" id="LogonForm">
+ <input type="hidden" name="storeId" value="10301" id="WC_AccountDisplay_FormInput_storeId_In_Logon_1">
+<input type="hidden" name="catalogId" value="10701" id="WC_AccountDisplay_FormInput_catalogId_In_Logon_1">
+<input type="hidden" name="langId" value="-1" id="WC_AccountDisplay_FormInput_langId_In__1">
+<input type="hidden" name="reLogonURL" value="LogonForm" id="WC_AccountDisplay_FormInput_reLogonURL_In_Logon_1">
+<input type="hidden" name="isPharmacy" value="" id="WC_AccountDisplay_FormInput_isPharmacy_In_Logon_1">
+<input type="hidden" name="authToken" value="312404731%2cKsqvty%2bpMJ%2bCAl3XeIkCxSEgLa4%3d">
+<input type="hidden" name="URL" value="CheckOutCmd?orderId=644156669&amp;storeId=10301&amp;storeId=10301&amp;authToken=312404731%252cKsqvty%252bpMJ%252bCAl3XeIkCxSEgLa4%253d&amp;authToken=312404731%252cKsqvty%252bpMJ%252bCAl3XeIkCxSEgLa4%253d&amp;orderErrMsgObj=%7B%7D&amp;itemMessage=1.0&amp;langId=-1&amp;langId=-1&amp;catalogId=10701&amp;catalogId=10701" id="WC_AccountDisplay_FormInput_URL_In_Logon_1">
+ <p>Please provide your email address and password to access your account.†
+ </p>
+ <div>
+<label for="logonId">Email Address<span>*</span>
+</label>
+<input id="logonId" name="logonId" maxlength="254" type="text"
+title="Email Address" value="">
+<br>
+ </div>
+ <div>
+ <label for="logonPassword">Password:<span>*</span>
+</label>
+ <input name="logonPassword" id="logonPassword" maxlength="40" type="password" autocomplete="off"
+title="Password:">
+ <p>Passwords are case sensitive.</p>
+ <br>
+ </div>
+ <div>
+ <input id="option1" name="option1" type="checkbox">
+<label for="option1">Remember me</label>
+ </div>
+ <input type="hidden" name="submitButton" value="signIn">
+ <div>
+<button type="submit"
+title="Sign in">
+<span>
+<span>Sign in</span>
+</span>
+</button>
+</div>
+ </form>
+ <form
+title="Reset password" name="ResetPasswordForm" method="post" action="https://www.costco.com/ResetPassword" id="ResetPasswordForm">
+ <input type="hidden" name="challengeAnswer" value="-" id="WC_PasswordResetForm_FormInput_challengeAnswer_In__1">
+ <input type="hidden" name="storeId" value="10301" id="WC_PasswordResetForm_FormInput_storeId_In__1">
+<input type="hidden" name="catalogId" value="10701" id="WC_PasswordResetForm_FormInput_catalogId_In__1">
+<input type="hidden" name="langId" value="-1" id="WC_PasswordResetForm_FormInput_langId_In__1">
+<input type="hidden" name="state" value="passwdconfirm" id="WC_PasswordResetForm_FormInput_state_In__1">
+<input type="hidden" name="URL" value="ResetPasswordSuccessView" id="WC_PasswordResetForm_FormInput_URL_In__1">
+<input type="hidden" name="errorViewName" value="RememberMeLogonFormView" id="WC_PasswordResetForm_FormInput_errorViewName_In__1">
+<input type="hidden" name="subject" value="New Costco.com Password" id="WC_PasswordResetForm_FormInput_subject_In__1">
+<input type="hidden" name="sender" value="no-reply@costco.com" id="WC_PasswordResetForm_FormInput_sender_In__1">
+<input type="hidden" name="isPharmacy" value="" id="WC_PasswordResetForm_FormInput_isPharmacy_In_Logon_1">
+ <p>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.
+ </p>
+ <div>
+<label for="forgotPassword_email">Email address<span>*</span>
+</label>
+<input id="forgotPassword_email" name="logonId" type="text"
+title="Email address">
+ </div>
+ <input type="hidden" name="submitButton" value="forgotPassword">
+ <div>
+<button type="submit"
+title="Reset password">
+<span>
+<span>Reset password</span>
+</span>
+</button>
+</div>
+ </form>
+ <form
+title="" name="RegisterForm" method="post" action="https://www.costco.com/UserRegistrationAdd" id="RegisterForm">
+ <input type="hidden" name="new" value="Y" id="WC_UserRegistrationAddForm_FormInput_new_In_Register_1">
+ <input type="hidden" name="storeId" value="10301" id="WC_UserRegistrationAddForm_FormInput_storeId_In_Register_1">
+ <input type="hidden" name="catalogId" value="10701" id="WC_UserRegistrationAddForm_FormInput_catalogId_In_Register_1">
+ <input type="hidden" name="langId" value="-1" id="WC_UserRegistrationAddForm_FormInput_langId_In__1">
+<input type="hidden" name="URL" value="CheckOutCmd?orderId=644156669&amp;storeId=10301&amp;storeId=10301&amp;authToken=312404731%252cKsqvty%252bpMJ%252bCAl3XeIkCxSEgLa4%253d&amp;authToken=312404731%252cKsqvty%252bpMJ%252bCAl3XeIkCxSEgLa4%253d&amp;orderErrMsgObj=%7B%7D&amp;itemMessage=1.0&amp;langId=-1&amp;langId=-1&amp;catalogId=10701&amp;catalogId=10701" id="WC_UserRegistrationAddForm_FormInput_URL_In_Register_1">
+ <input type="hidden" name="userField1" value="" id="WC_UserRegistrationAddForm_FormInput_userField1_In_Register_1">
+ <input type="hidden" name="addressField1" value="" id="WC_UserRegistrationAddForm_FormInput_addressField1_In_Register_1">
+ <input type="hidden" name="addressType" value="B" id="WC_UserRegistrationAddForm_FormInput_addressType_In_Register_1">
+ <input type="hidden" name="nickName" value="Self Address" id="WC_UserRegistrationAddForm_FormInput_nickName_In_Register_1">
+ <input type="hidden" name="errorViewName" value="LogonForm" id="WC_UserRegistrationAddForm_FormInput_errorViewName_In_Register_1">
+ <input type="hidden" name="validated" id="validated" value="true">
+ <input type="hidden" name="primary" value="false" id="WC_UserRegistrationAddForm_FormInput_primary_In_Register_1">
+ <input type="hidden" name="challengeQuestion" value="-" id="WC_UserRegistrationAddForm_FormInput_challengeQuestion_In_Register_1">
+ <input type="hidden" name="challengeAnswer" value="-" id="WC_UserRegistrationAddForm_FormInput_challengeAnswer_In_Register_1">
+ <input type="hidden" name="fromPage" value="LogonForm" id="WC_UserRegistrationAddForm_FormInput_fromPage_In_Register_1">
+ <input type="hidden" name="isPharmacy" value="" id="WC_UserRegistrationAddForm_FormInput_isPharmacy_In_Logon_1">
+ <input type="hidden" name="page" value="account" id="WC_UserRegistrationAddForm_FormInput_page_In_Register_1">
+ <input type="hidden" name="parentMember" value="o=costco us bc sellers,o=costco na sellers,o=extended sites seller organization,o=root organization">
+ <p>Enter your email address and create a password below to register.†
+ </p>
+ <div>
+<span>*</span> Required fields
+ </div>
+ <div>
+<label for="register_email1">Email Address<span>*</span>
+</label>
+<input id="register_email1" name="email1" type="text" maxlength="40"
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: Email Address* parseable name: email1 field signature: 1119374200 form signature: 14385182823106756929" value=""
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ <div id="passwordField">
+ <label for="register_logonPassword">Password:</label>
+<input id="register_logonPassword" name="logonPassword" maxlength="20" type="password" value=""
+title="overall type: ACCOUNT_CREATION_PASSWORD server type: ACCOUNT_CREATION_PASSWORD heuristic type: UNKNOWN_TYPE label: Password: parseable name: logonPassword field signature: 354853082 form signature: 14385182823106756929"
+autofill-prediction="ACCOUNT_CREATION_PASSWORD"
+>
+ <div id="PasswordStrength">
+ <img src="./Sign%20In_files/Password_Strength_Arrow.png">
+ <div>
+ <p>Password must meet the following:</p>
+ <div>
+ <ul>
+ <li>Use between 8 and 20 characters</li>
+ <li>Include at least one letter</li>
+ <li>Does not contain blank spaces or the following special characters: &lt; &gt; " \ .
+ </li>
+ <li>Passwords match</li>
+ </ul>
+ </div>
+ <p>Password Strength : <span id="strengthText">
+</span>
+</p>
+ <ul>
+ <li>Too Short</li>
+ <li>Weak</li>
+ <li>Fair</li>
+ <li>Good</li>
+ <li>Strong</li>
+ </ul>
+ <p id="passwordStrengthBar">
+</p>
+ <p>To improve strength, increase password length and use capital letters, numbers, and special characters
+ (except &lt; &gt; " \ .)
+ </p>
+ </div>
+ </div>
+ </div>
+ <div>
+<label for="register_logonPasswordVerify">Confirm Password<span>*</span>
+</label>
+<input id="register_logonPasswordVerify" name="logonPasswordVerify" maxlength="20" type="password" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Confirm Password* parseable name: logonPasswordVerify field signature: 1976176530 form signature: 14385182823106756929"
+autofill-prediction="UNKNOWN_TYPE"
+>
+</div>
+ <div>
+<label for="register_userField2">Costco Membership Number</label>
+<input id="register_userField2" name="userField2" type="text" maxlength="16"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Costco Membership Number parseable name: userField2 field signature: 1051463506 form signature: 14385182823106756929" value=""
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <p>
+<b>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.</b>
+ </p>
+ <div>
+ <input id="register_sendMeEmail" name="sendMeEmail" type="checkbox" checked="checked"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Yes, I would like to receive emails about special offers and new product information from Costco. Co parseable name: sendMeEmail field signature: 3147026083 form signature: 14385182823106756929"
+autofill-prediction="UNKNOWN_TYPE"
+>
+<label for="register_sendMeEmail">Yes, I would like to receive emails about special offers and new product information from Costco. Costco will not rent or sell your email address.</label>
+ </div>
+ <input name="submitButton" value="Register" type="hidden">
+ <div>
+<button type="submit"
+title="Register">
+<span>
+<span>Register</span>
+</span>
+</button>
+</div>
+ </form>
+ <form action="https://www.costco.com/EmailSubscription" id="footer_emailSignup" name="footer_emailSignup">
+ <input id="footer_emailSignUpEmail" name="emailSignUp" type="text">
+ </form>
+ <div id="language-region-modal-container">
+ <div>
+ ???LANGUAGE_REGION_MODAL_TITLE???
+ </div>
+ <div>
+ <div id="language-radio-buttons">
+ <p>???LANGUAGE_REGION_MODAL_CHOOSE_LANGUAGE???</p>
+ <label>
+ <input type="radio" name="language" value="-1">???HEADER_LANGUAGE_NAME_-1???</label>
+ </div>
+ <hr>
+ <div id="region-radio-buttons">
+ <p>???LANGUAGE_REGION_MODAL_CHOOSE_REGION???</p>
+ <div>
+ <label>
+ <input type="radio" name="region" value="AB">Alberta - AB</label>
+ <label>
+ <input type="radio" name="region" value="BC">British Columbia - BC</label>
+ <label>
+ <input type="radio" name="region" value="MB">Manitoba - MB</label>
+ <label>
+ <input type="radio" name="region" value="NB">New Brunswick - NB</label>
+ <label>
+ <input type="radio" name="region" value="NL">Newfoundland and Labrador - NL</label>
+ <label>
+ <input type="radio" name="region" value="NT">Northwest Territories - NT</label>
+ <label>
+ <input type="radio" name="region" value="NS">Nova Scotia - NS</label>
+ </div>
+ <div>
+ <label>
+ <input type="radio" name="region" value="NU">Nunavut - NU</label>
+ <label>
+ <input type="radio" name="region" value="ON">Ontario - ON</label>
+ <label>
+ <input type="radio" name="region" value="PE">Prince Edward Island - PE</label>
+ <label>
+ <input type="radio" name="region" value="QC">Quebec - QC</label>
+ <label>
+ <input type="radio" name="region" value="SK">Saskatchewan - SK</label>
+ <label>
+ <input type="radio" name="region" value="YT">Yukon - YT</label>
+ </div>
+ </div>
+ <div>
+ <input id="language-region-set" type="submit" value="???LANGUAGE_REGION_MODAL_SUBMIT???">
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Direct.asda.com</title>
+</head>
+
+<body>
+ <form class="card-form">
+ <div class="form-group"><label class="label" for="cardNumber">Card number</label>
+ <div>
+ <div class="input-group"><input class="form-control error" name="cardNumber" type="tel" autocomplete="on"
+ data-id="input-payment-add-card-number" value=""></div><span data-id="input-payment-add-card-number"
+ class="error">Please enter a card number</span>
+ </div>
+ </div>
+ <div class="form-group"><label class="label" for="cardHolder">Name on card</label>
+ <div>
+ <div class="input-group"><input class="form-control" name="cardHolder" type="text" autocomplete="on"
+ data-id="input-payment-add-card-name-on-card" value=""></div>
+ </div>
+ </div>
+ <div class="grid__list card-grid">
+ <div class="form-group cvv-form-group"><label class="label" for="cvv">CVV number</label>
+ <div>
+ <div class="input-group flex-grid flex-grid--center"><input class="form-control" name="cvv" type="tel"
+ autocomplete="on" data-id="input-payment-add-card-cvv" value=""><span class="icon cvv-code-icon"><svg
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="36" height="24">
+ <defs>
+ <rect id="cvv-dep-a" width="36" height="24" rx="2.182"></rect>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <rect width="36" height="24" fill="#D8D8D8" fill-rule="nonzero" rx="2.182"></rect>
+ <mask id="cvv-dep-b" fill="#fff">
+ <use xlink:href="#cvv-dep-a"></use>
+ </mask>
+ <path fill="#191919" fill-rule="nonzero" d="M0 0h36v5.526H0z" mask="url(#cvv-dep-b)"></path>
+ <rect width="6.545" height="4.435" x="26.727" y="9.282" stroke="#D8365A" stroke-width="1.091"
+ rx=".545"></rect>
+ <path fill="#FFF" fill-rule="nonzero" d="M2.182 8.737H24v5.526H2.182z"></path>
+ </g>
+ </svg></span><span class="icon cvv-code-helper"><svg xmlns="http://www.w3.org/2000/svg" width="24"
+ height="24">
+ <g fill="none" fill-rule="evenodd">
+ <circle fill="#191919" cx="12" cy="12" r="12"></circle><text font-family="Arial-BoldMT, Arial"
+ font-size="15" font-weight="bold" fill="#FFF">
+ <tspan x="7" y="17.235">?</tspan>
+ </text>
+ </g>
+ </svg></span></div>
+ </div>
+ </div>
+ <div class="grid__list adjust-date"><label class="label">Expiry date</label>
+ <div>
+ <div class="flex-grid--inline">
+ <div class="flex-grid--inline">
+ <div class="form-group expiry-month">
+ <div>
+ <div class="input-group"><input class="form-control" name="month" type="tel" placeholder="MM"
+ data-id="input-payment-add-card-expiry-month" value=""></div>
+ </div>
+ </div><span class="date-separator">/</span>
+ <div class="form-group expiry-year">
+ <div class="input-group">
+ <div>
+ <div class="input-group"><input class="form-control" name="year" type="tel" placeholder="YY"
+ data-id="input-payment-add-card-expiry-year" value=""></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div></div>
+ </div>
+ </div>
+ </div>
+ <div class="save-card-text">
+ <div>
+ <div class="checkbox-section size-normal"><input class="checkbox-field" name="save" id="save"
+ data-id="checkbox-payment-save-card" type="checkbox"><label for="save"
+ class="custom-checkbox"></label><label for="save" class="checkbox-label">Save card</label></div>
+ </div>
+ </div>
+ </div>
+ </form>
+</body>
+
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="fr">
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Checkout Payment - Ebay - FR</title>
+</head>
+
+<body class="xo desktop">
+ <div id="root">
+ <div class="bodyContent" tabindex="-1">
+ <div userid="hadox-45" isebayintermediated="true" aria-hidden="true"></div>
+ <div id="gh-gb" tabindex="-1"></div>
+ <h1 class="page-title">Finalisation de l'achat</h1>
+ <div class="credit-card-container">
+ <div class="form-element card-number">
+ <div class="credit-card-number" aria-live="polite">
+ <div class="float-label expanded"><label for="cardNumber">Numéro de carte</label>
+ <div><input aria-required="true" data-validations="REQUIRED_FIELD" class="" autocomplete="cc-number"
+ data-val_required_field_params="{}" id="cardNumber" type="tel" name="cardNumber"
+ value="4111 1111 1111 1111" error=""></div>
+ <div class="card-types" aria-live="polite"><span aria-hidden="false" class="payment-logo VISA small"
+ aria-label="VISA" role="img"></span></div>
+ </div>
+ </div>
+ </div>
+ <div class="">
+ <div class="form-element">
+ <div class="float-label expanded"><label for="cardExpiryDate">Date d'expiration</label>
+ <div><input autocomplete="cc-exp" class="" aria-describedby="cardExpiryDate-accessorylabel"
+ aria-required="true" data-val_month_and_year_format_params="{}" data-val_required_field_params="{}"
+ id="cardExpiryDate" type="tel" name="cardExpiryDate" placeholder="MM / AA"
+ data-validations="MONTH_AND_YEAR_FORMAT,REQUIRED_FIELD" value="02/23" error=""></div>
+ <div id="cardExpiryDate-accessorylabel" class="secondary-text"></div>
+ </div>
+ </div>
+ <div class="form-element card-cvv">
+ <div class="float-label"><label for="securityCode">Code de sécurité</label>
+ <div><input autocomplete="cc-csc" maxlength="3" optionaltext="Facultatif pour une carte de débit."
+ pattern="[0-9]*" id="securityCode" type="tel" name="securityCode" class=""
+ placeholder="3 ou 4 chiffres" data-validations="CVV_NUMBER" value="" cardnumber="4111 1111 1111 1111"
+ data-val_cvv_number_params="{}"></div>
+ <div class="bubble"><span class="bubble-child"><span class="bubblehelp"><span class="infotip "><button
+ tabindex="0" aria-expanded="false" aria-label="En savoir plus sur le code de sécurité."
+ class="icon-btn infotip__host" type="button"><svg height="24px" width="24px"
+ class="icon icon--information " xmlns="http://www.w3.org/2000/svg" focusable="false"
+ aria-hidden="true">
+ <use xlink:href="#icon-information"></use>
+ </svg></button><span class="infotip__overlay" role="tooltip"
+ style="inset: 218.3px auto auto 709.7px; position: fixed;"><span
+ class="infotip__pointer infotip__pointer--top-left"></span><span class="infotip__mask"><span
+ class="infotip__cell"><span class="infotip__content">
+ <div class="ICON"><span class="loadable-icon-and-text"><span
+ class="visa-cvv loadable-icon-and-text-icon" aria-hidden="true"></span></span></div>
+ <div class="ICON"><span class="loadable-icon-and-text"><span
+ class="amex-cvv loadable-icon-and-text-icon" aria-hidden="true"></span></span></div>
+ <div class="TITLE"><span class="loadable-icon-and-text"><span class="text-display"><span
+ class="">Visa, Mastercard ou Discover</span></span></span></div>
+ <ul>
+ <li class="DETAILS_LIST"><span class="loadable-icon-and-text"><span
+ class="text-display"><span class="">Il s'agit du numéro à 3&nbsp;chiffres situé au
+ verso de votre carte, à côté de l'emplacement réservé à la
+ signature.</span></span></span></li>
+ </ul>
+ <div class="TITLE"><span class="loadable-icon-and-text"><span class="text-display"><span
+ class="">American Express</span></span></span></div>
+ <ul>
+ <li class="DETAILS_LIST"><span class="loadable-icon-and-text"><span
+ class="text-display"><span class="">Il s'agit du numéro à 4&nbsp;chiffres situé au
+ recto de votre carte au-dessus de son numéro.</span></span></span></li>
+ </ul>
+ </span><button aria-label="Close CVV Information Overlay" class="infotip__close"
+ type="button"><svg height="24px" width="24px" class="icon icon--close "
+ xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true">
+ <use xlink:href="#icon-close"></use>
+ </svg></button></span></span></span></span></span></span></div>
+ </div>
+ </div>
+ </div>
+ <div class="form-element">
+ <div class="float-label expanded"><label for="cardHolderFirstName">Prénom</label>
+ <div><input autocomplete="cc-given-name" aria-required="true"
+ aria-describedby="cardHolderFirstName-accessorylabel" data-val_required_field_params="{}"
+ id="cardHolderFirstName" type="text" name="cardHolderFirstName" class=""
+ placeholder="Saisissez le prénom sur la carte" data-validations="REQUIRED_FIELD" value="John"></div>
+ <div id="cardHolderFirstName-accessorylabel" class="secondary-text"></div>
+ </div>
+ </div>
+ <div class="form-element">
+ <div class="float-label expanded"><label for="cardHolderLastName">Nom</label>
+ <div><input autocomplete="cc-family-name" aria-required="true"
+ aria-describedby="cardHolderLastName-accessorylabel" data-val_required_field_params="{}"
+ id="cardHolderLastName" type="text" name="cardHolderLastName" class=""
+ placeholder="Saisissez le nom sur la carte" data-validations="REQUIRED_FIELD" value="Smith"></div>
+ <div id="cardHolderLastName-accessorylabel" class="secondary-text"></div>
+ </div>
+ </div>
+ <div class="form-element remember-card"><span class="checkbox-wrapper field"><span
+ class="checkbox field__control"><svg style="display: none;">
+ <symbol id="icon-checkbox-checked" viewBox="0 0 22 22">
+ <path fill-rule="evenodd"
+ d="M1 0h20a1 1 0 011 1v20a1 1 0 01-1 1H1a1 1 0 01-1-1V1a1 1 0 011-1zm7.3 15.71a1 1 0 001.41 0l8-8h-.01a1 1 0 00-1.41-1.41L9 13.59 5.71 10.3a1 1 0 00-1.41 1.41l4 4z">
+ </path>
+ </symbol>
+ <symbol id="icon-checkbox-checked-small" viewBox="0 0 14 14">
+ <path
+ d="M13 0H1a1 1 0 00-1 1v12a1 1 0 001 1h12a1 1 0 001-1V1a1 1 0 00-1-1zm-2.29 5.71l-4 4a1 1 0 01-1.41 0l-2-2a1 1 0 011.41-1.42L6 7.59 9.29 4.3a1 1 0 011.41 1.41h.01z">
+ </path>
+ </symbol>
+ <symbol id="icon-checkbox-unchecked" viewBox="0 0 21 22">
+ <path fill-rule="evenodd"
+ d="M.955 0h19.09c.528 0 .955.448.955 1v20c0 .552-.427 1-.955 1H.955C.427 22 0 21.552 0 21V1c0-.552.427-1 .955-1zm.954 20h17.182V2H1.909v18z">
+ </path>
+ </symbol>
+ <symbol id="icon-checkbox-unchecked-small" viewBox="0 0 14 14">
+ <path d="M13 14H1a1 1 0 01-1-1V1a1 1 0 011-1h12a1 1 0 011 1v12a1 1 0 01-1 1zM2 12h10V2H2v10z"></path>
+ </symbol>
+ </svg><input for="rememberCard" id="rememberCard" class="checkbox__control " type="checkbox"
+ checked=""><span class="checkbox__icon" hidden=""><svg height="24px" width="24px"
+ class="checkbox__checked" xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true">
+ <use xlink:href="#icon-checkbox-checked"></use>
+ </svg><svg height="24px" width="24px" class="checkbox__unchecked" xmlns="http://www.w3.org/2000/svg"
+ focusable="false" aria-hidden="true">
+ <use xlink:href="#icon-checkbox-unchecked"></use>
+ </svg></span></span><label class="field__label field__label--end" for="rememberCard"><span
+ class="text-display"><span class="">Enregistrer cette carte pour de futurs
+ achats</span></span></label></span></div>
+ </div>
+ </div>
+ </div>
+</body>
+
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Global.direct.asda.com</title>
+</head>
+
+<body>
+ <form action="/1/Payments/HandleCreditCardRequestV2?mode=13534" method="post" id="paymentFrm" novalidate="novalidate">
+ <div class="clearfix" id="secureContainer" data-culture="en-GB" data-direction="ltr">
+ <div class="form-horizontal">
+ <div class="form-group has-error has-feedback">
+
+ <label for="cardNum" class="col-sm-4 col-xs-12 control-label fcap paylabel">Card number<div data-show="true"
+ class="glyphicon glyphicon-star astrsk"></div></label>
+ <div class="col-sm-8 col-xs-12 fval" id="CreditCardCell" data-cc-valid="false">
+ <input aria-label="Card number" autocomplete="off" class="form-control input-validation-error"
+ data-type="unknown" data-type-id="1" data-val="true" data-val-luhn="Card number not valid"
+ data-val-luhn-allowempty="False" data-val-luhn-allowspaces="False" data-val-required="Card number"
+ id="cardNum" name="PaymentData.cardNum" pattern="[0-9]{13,16}" placeholder="Card number" type="tel"
+ value="" aria-required="true"><span class="glyphicon glyphicon-remove form-control-feedback"
+ aria-hidden="true"></span>
+ <div id="cardTypeInfo">
+ <div class="isvisa pm_visa pm_general"></div>
+ <div class="ismastercard pm_mastercard pm_general"></div>
+ <div class="isamex pm_amex pm_general"></div>
+ <div class="ismaestro pm_maestro pm_general"></div>
+ <div class="isjcb pm_jcb pm_general"></div>
+ <div class="isdiners pm_diners pm_general"></div>
+ <div class="isdiscover pm_discover pm_general"></div>
+ <div class="ismir pm_mir pm_general"></div>
+ <div class="isdefault pm_default pm_general"></div>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="cardExpiryMonth" class="col-sm-4 col-xs-12 control-label fcap paylabel">Expiry date<div
+ data-show="true" class="glyphicon glyphicon-star astrsk"></div></label>
+ <div class="col-sm-8 col-xs-12 fval">
+ <div class="row" id="expDateRow">
+
+ <div class="col-xs-6">
+ <div class="FSelect">
+ <div class="arrow"></div>
+ <div class="FCurValue">Month</div><select aria-label="Month" class="form-control"
+ data-tooltip-special-pos="top left;bottom left" data-val="true"
+ data-val-datemustbeequalorgreaterthancurrentdate="The credit card expiration date you provided has already expired."
+ data-val-required="Expiry date" data-widget="lightcombobox" id="cardExpiryMonth"
+ name="PaymentData.cardExpiryMonth" data-rendered="true" aria-required="true">
+ <option selected="selected" value="">Month</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="col-xs-6">
+ <div class="FSelect">
+ <div class="arrow"></div>
+ <div class="FCurValue">Year</div><select aria-label="Year" class="form-control"
+ data-tooltip-special-pos="top right;bottom right" data-val="true"
+ data-val-datemustbeequalorgreaterthancurrentdate="The credit card expiration date you provided has already expired."
+ data-val-required="Expiry date" data-widget="lightcombobox" id="cardExpiryYear"
+ name="PaymentData.cardExpiryYear" data-rendered="true" aria-required="true">
+ <option selected="selected" value="">Year</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ <option value="2028">2028</option>
+ <option value="2029">2029</option>
+ <option value="2030">2030</option>
+ <option value="2031">2031</option>
+ <option value="2032">2032</option>
+ <option value="2033">2033</option>
+ <option value="2034">2034</option>
+ <option value="2035">2035</option>
+ <option value="2036">2036</option>
+ <option value="2037">2037</option>
+ <option value="2038">2038</option>
+ <option value="2039">2039</option>
+ <option value="2040">2040</option>
+ <option value="2041">2041</option>
+ <option value="2042">2042</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="cvvContainer" class="form-group">
+ <label for="cvdNumber" class="col-sm-4 col-xs-12 control-label fcap paylabel">Security code<div
+ data-show="true" class="glyphicon glyphicon-star astrsk"></div></label>
+ <div class="col-sm-8 col-xs-12 fval">
+ <input aria-label="Security code" autocomplete="off" class="form-control" data-tt-pos="top" data-val="true"
+ data-val-cvvval="Please enter a valid CVV" data-val-cvvval-otherpropertyname=""
+ data-val-required="Security code" id="cvdNumber" name="PaymentData.cvdNumber" pattern="[0-9]{3,4}"
+ placeholder="CVV" type="tel" value="" aria-required="true">
+ <span id="cvvDescriptionContainer" data-toggle="tooltip" data-placement="top" title=""
+ data-original-title="The&nbsp;Security Code&nbsp;on your credit card or debit card is a 3 digit number on the back of your VISA®, MasterCard® and Discover® branded credit and debit cards. On your American Express® branded credit or debit card it is a 4 digit numeric code on the front of the card.">
+ <label tabindex="0" class="control-label" id="cvvInfo"> What is this?</label>
+ <label class="control-label secureinfolabel"><span class="secureInfo lazy"></span></label>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!--#region Hidden Payment Parameters-->
+ <input type="hidden" name="PaymentData.checkoutV2" value="true">
+ <input type="hidden" name="PaymentData.cartToken" id="cartToken" value="045078e1-dafa-4d42-8e1a-a8a8dc3c2234">
+ <input type="hidden" name="PaymentData.gatewayId" id="gatewayId" value="2">
+ <input type="hidden" name="PaymentData.paymentMethodId" id="paymentMethodId" value="1">
+ <input type="hidden" name="PaymentData.machineId" id="machineId">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.createTransaction" id="createTransaction" value="true">
+ <input type="hidden" name="PaymentData.checkoutCDNEnabled" id="checkoutURL" value="value">
+ <input type="hidden" name="PaymentData.recapchaToken" id="recapchaToken">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.recapchaTime" id="recapchaTime">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.customerScreenColorDepth" id="customerScreenColorDepth">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.customerScreenWidth" id="customerScreenWidth">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.customerScreenHeight" id="customerScreenHeight">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.customerTimeZoneOffset" id="customerTimeZoneOffset">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.customerLanguage" id="customerLanguage">
+ <!--Needs to be updated by script-->
+ <input type="hidden" name="PaymentData.UrlStructureTokenEncoded" id="UrlStructureTokenEncoded"
+ value="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFbmNvZGVkTWVyY2hhbnRJZCI6IjhybzgiLCJDYXJ0VG9rZW4iOiIwNDUwNzhlMS1kYWZhLTRkNDItOGUxYS1hOGE4ZGMzYzIyMzQiLCJJc1JlcXVpcmVkVG9QYXlXaXRoRGVjb2RlZE1lcmNoYW50SWRBbmRUb2tlbkluVXJsIjpmYWxzZX0.NaxXTwl3w9OurCnPmosjy0P0kSvvs9JfY1OnRVI_w_4">
+ <!--#endregion-->
+ </form>
+</body>
+
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>The Home Depot - Checkout</title>
+ </head>
+ <body>
+ <checkout-form name="pickupDetailsSection">
+ <shipping>
+ <div>
+ <hd-address type="shipping">
+ <hd-address-field label="Shipping Address" type="type">
+ <div name="addressFieldForm">
+ <div>
+ <div>
+ <div>
+ <hd-name-field name="firstName" label="First Name" analytics-tag="pickup options">
+ <span name="firstName">
+ <label for="inputField">
+<span>First Name</span>
+</label>
+<input type="text" id="firstName" name="inputField" maxlength="30" placeholder="" required="required">
+ </span>
+ </hd-name-field>
+ </div>
+ <div>
+ <hd-name-field name="lastName" label="Last Name" analytics-tag="pickup options">
+ <span name="lastName">
+ <label for="inputField">
+<span>Last Name</span>
+</label>
+<input type="text" id="lastName" name="inputField" maxlength="30" placeholder="" required="required">
+ </span>
+ </hd-name-field>
+ </div>
+ </div>
+ <div>
+ <div>
+ <hd-email-field name="emailInput" label="Email" placeholder="you@domain.com" analytics-tag="pickup options">
+ <span name="emailInput">
+ <label for="inputField">
+<span>Email</span>
+</label>
+<input id="emailInput" type="email" name="inputField" placeholder="you@domain.com" required="required">
+ </span>
+ </hd-email-field>
+ </div>
+ </div>
+ <div>
+ <create-account>
+ <div>
+ <div>
+ <div>
+<span role="button" tabindex="0">Create an account</span> to track your order history and check out faster - all we need is a password.</div>
+ <div>
+ <p>Check out faster, access past orders, and organize products into lists.</p>
+ </div>
+ </div>
+ <div>
+ <div>
+ <hd-password-field label="Password" name="password">
+<span name="hdPasswordField">
+<label for="textPasswordInput">
+<span>Password</span>
+</label>
+<input type="password" name="inputField">
+<span>
+</span>
+</span>
+</hd-password-field>
+ </div>
+ <div>
+ <hd-password-field name="confirmPassword" label="Confirm Password">
+<span name="hdPasswordField">
+<label for="textPasswordInput">
+<span>Confirm Password</span>
+</label>
+<input type="password" name="inputField">
+<span>
+</span>
+</span>
+</hd-password-field>
+ </div>
+ <div>
+ <div>
+ <hd-check-box field-value="" label="SHOW PASSWORD" tab-index-hd="-1">
+ <div>
+ <div>
+<input tabindex="-1" type="checkbox" name="hdCheckBox_3" id="hdCheckBox_3">
+<label for="hdCheckBox_3">SHOW PASSWORD</label>
+</div>
+ </div>
+ </hd-check-box>
+ </div>
+ <div>
+ <span>Passwords are case sensitive and must be at least 8 characters.</span>
+ <div>
+ <span>Create a strong password by:</span>
+ <ul>
+ <li>Including numbers or symbols</li>
+ <li>Mixing upper/lowercase</li>
+ </ul>
+ </div>
+ </div>
+ <div>
+<a target="_blank">Terms &amp; Conditions</a> &nbsp;|&nbsp; <a target="_blank">Privacy &amp; Security</a>
+</div>
+ </div>
+ <create-account-button>
+</create-account-button>
+ </div>
+ </div>
+ </create-account>
+ </div>
+ <div>
+ <div>
+ <hd-phone-field name="phone" label="Phone" analytics-tag="pickup options">
+ <span name="phone">
+ <label for="inputField">
+<span>Phone</span>
+</label>
+<input id="phone" name="inputField" type="tel" inputmode="numeric" placeholder="(___) ___-____" required="required">
+ </span>
+ </hd-phone-field>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="billingAddress">
+<span>Shipping Address</span>
+</label>
+ </div>
+ <span name="streetInput">
+ <hd-type-ahead id="billingAddress" name="billingAddress" label="Billing Street Address" placeholder="Address Line 1" pause="700" input-class="form-input__field">
+ <div>
+ <input id="billingAddress_value" name="billingAddress" type="text" maxlength="30" placeholder="Address Line 1" required="">
+ </div>
+ </hd-type-ahead>
+ </span>
+ </div>
+ </div>
+ <div>
+<span role="button" tabindex="0">Add an apartment, suite, building, etc.</span>
+</div>
+ <div>
+ <div>
+ <span name="zipInput">
+ <label for="zip">
+<span>ZIP Code</span>
+</label>
+<input type="tel" name="zip" maxlength="5" required="">
+ </span>
+ </div>
+ <div>
+ <span>
+ <label>
+<span>City, State</span>
+</label>
+ <span>
+<b>MOUNTAIN VIEW, CA</b>
+</span>
+ <div>
+ <span name="stateInput">
+ <select id="cityStateListSelector" autocomplete="billing street-address" name="pickupLocation">
+ <option value="? object:null ?" selected="selected">
+</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </hd-address-field>
+ </hd-address>
+ <div>
+ <hd-check-box field-value="checkoutFlags.hideBillingAddress" label="Use as Billing Address" checked="checked">
+ <div>
+ <div>
+<input tabindex="" type="checkbox" name="hdCheckBox_1" id="hdCheckBox_1" checked="checked">
+<label for="hdCheckBox_1">Use as Billing Address</label>
+</div>
+ </div>
+ </hd-check-box>
+ </div>
+ </div>
+ </shipping>
+ </checkout-form>
+ <card-field form-is-valid="formIsValid">
+ <div name="cardForm">
+ <div>
+<input name="expiryMonth" type="tel" autocomplete="cc-exp-month" tabindex="-1">
+<input name="expiryYear" type="tel" autocomplete="cc-exp-year" tabindex="-1">
+</div>
+ <div>
+ <div>
+ <span>Payment</span>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="radio" value="PayPal" id="payPal" name="paymentOptions">
+ <label for="payPal" tabindex="10">
+ <span>
+</span>
+ <div>
+</div>
+ </label>
+ </div>
+ <div>
+<input type="radio" select-payment="" value="creditCard" id="creditCard" name="paymentOptions" checked="checked">
+<label for="creditCard" tabindex="10">
+<span>
+</span>Credit Card</label>
+</div>
+ <div>
+ <span>
+ <label>
+ </label>
+ <input id="cardNumber" name="cardNumber" type="tel" mask-field="" mask="0000 0000 0000 0000 0000 0" maxlength="26" placeholder="Enter credit card number" required="required">
+<span id="ccIcon" tabindex="-1" role="button">
+</span>
+ </span>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <span>Expiration</span>
+ <span>
+ <select id="ccMonth" name="ccMonth" required="">
+ <option value="" selected="selected">Month</option>
+ <option label="01 - January" value="object:17">01 - January</option>
+ <option label="02 - February" value="object:18">02 - February</option>
+ <option label="03 - March" value="object:19">03 - March</option>
+ <option label="04 - April" value="object:20">04 - April</option>
+ <option label="05 - May" value="object:21">05 - May</option>
+ <option label="06 - June" value="object:22">06 - June</option>
+ <option label="07 - July" value="object:23">07 - July</option>
+ <option label="08 - August" value="object:24">08 - August</option>
+ <option label="09 - September" value="object:25">09 - September</option>
+ <option label="10 - October" value="object:26">10 - October</option>
+ <option label="11 - November" value="object:27">11 - November</option>
+ <option label="12 - December" value="object:28">12 - December</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ <div>
+ <div>
+ <span>
+ <select id="ccYear" name="ccYear" required="">
+ <option value="" selected="selected">Year</option>
+ <option label="2017" value="object:29">2017</option>
+ <option label="2018" value="object:30">2018</option>
+ <option label="2019" value="object:31">2019</option>
+ <option label="2020" value="object:32">2020</option>
+ <option label="2021" value="object:33">2021</option>
+ <option label="2022" value="object:34">2022</option>
+ <option label="2023" value="object:35">2023</option>
+ <option label="2024" value="object:36">2024</option>
+ <option label="2025" value="object:37">2025</option>
+ <option label="2026" value="object:38">2026</option>
+ <option label="2027" value="object:39">2027</option>
+ <option label="2028" value="object:40">2028</option>
+ <option label="2029" value="object:41">2029</option>
+ <option label="2030" value="object:42">2030</option>
+ <option label="2031" value="object:43">2031</option>
+ <option label="2032" value="object:44">2032</option>
+ <option label="2033" value="object:45">2033</option>
+ <option label="2034" value="object:46">2034</option>
+ <option label="2035" value="object:47">2035</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ </div>
+ <div>
+ <span>
+ <label>
+ <span>
+ CVV (on back)
+ </span>
+ </label>
+ <input id="cvv" name="cvv" type="tel" placeholder="ñññ" minlength="3" required="">
+<span id="ccIcon">
+</span>
+ </span>
+ </div>
+ </div>
+ <div>
+ <span role="button" tabindex="0">Apply a Gift Card</span>
+ <span>&nbsp;|&nbsp;</span>
+ <span role="button" tabindex="0">Have a PO/Job Code for this order?</span>
+ </div>
+ </div>
+ <div>
+ </div>
+ <div>
+ </div>
+ </div>
+ </card-field>
+ <base>
+ <div>
+ <ui-view>
+ <div id="checkout" analytics="">
+ <div name="checkout">
+ <div>
+ <div>
+ <div>
+ <myapron-display>
+ <div>
+ <div>
+<span name="myApronID">
+<label for="myapron">
+<span>myApron ID (Optional)</span>
+</label>
+<input type="tel" name="myapron" maxlength="10">
+</span>
+</div>
+ </div>
+ </myapron-display>
+ <email-subscribe>
+ <div>
+ <div>
+ <hd-check-box field-value="user.emailSubscribed" name="emailSubscribed" label="Yes, I would like to receive emails about unadvertised &amp; online only specials, new products and store promotions." checked="true">
+ <div>
+ <div>
+<input tabindex="" type="checkbox" name="emailSubscribed" id="hdCheckBox_2" checked="checked">
+<label for="hdCheckBox_2">Yes, I would like to receive emails about unadvertised &amp; online only specials, new products and store promotions.</label>
+</div>
+ </div>
+ </hd-check-box>
+ </div>
+ </div>
+ </email-subscribe>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <right-rail pick-up-options="pickUpOptions" messages="checkoutModel.messagesSummary">
+ <div>
+ <div>
+ <promotions-summary promotions="order.promotionsModel" messages="messages">
+ <div name="promoForm">
+ <div>
+<span role="button" tabindex="0">Have a promo code?</span>
+</div>
+ <div>
+ <div>
+<span>
+<span>
+</span>
+</span>
+</div>
+ </div>
+ <div>
+ <span>
+<input id="promoId" name="promoId" type="text">
+</span>
+<span>
+<a>
+<span>Apply</span>
+</a>
+</span>
+ </div>
+ </div>
+ </promotions-summary>
+ </div>
+ </div>
+ </right-rail>
+ </div>
+ </div>
+ </ui-view>
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>The Home Depot - SignIn</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta content="yes" name="apple-mobile-web-app-capable">
+ <meta content="black" name="apple-mobile-web-app-status-bar-style">
+ <meta content="&nbsp;at The Home Depot - Tablet" name="description">
+ <meta content="" name="keywords">
+ </head>
+ <body>
+ <form name="shoppingCartForm" id="shopCartForm" method="post">
+ <input type="hidden" id="lineItemId" name="lineItemId" value="480de313-1be8-41ed-b8f9-1b1e371a7eb9_480de313-1be8-41ed-b8f9-1b1e371a7eb9">
+ <input type="hidden" id="quantity" name="quantity" value="">
+ <input type="hidden" id="fulfillmentMethod" name="fulfillmentMethod" value="ShipToHome">
+ <input type="hidden" id="fulfillmentLocation" name="fulfillmentLocation" value="">
+ <input type="hidden" id="LOCAL_STORE_ID" name="LOCAL_STORE_ID" value="">
+ <input type="hidden" id="orderId" name="orderId" value="711220840">
+ <input type="hidden" name="proceedAsGuest" value="yes">
+ <input type="hidden" id="currentPage" name="currentPage" value="LogonPage">
+ <input type="hidden" id="cartVisited" name="cartVisited" value="false">
+ <input type="hidden" id="isEligibleForOPC" name="isEligibleForOPC" value="true">
+ <input type="hidden" id="paymentType" name="paymentType" value="regularCheckout">
+ <div>
+<span>
+<label>
+<span>Email Address:</span>
+</label>
+ <input type="email" placeholder="you@domain.com" id="guestEmail" name="guestLoginValue" value="" required="" errorkey="email">
+</span>
+ </div>
+ <div>
+ <p>You will have the opportunity to create an account and track your order once you complete your checkout.
+ </p>
+ <p>
+</p>
+ </div>
+ <div>
+<button>
+<span>Continue</span>
+</button>
+</div>
+ </form>
+ <form name="checkOutLogonForm" id="checkOutLogonForm" method="post" action="https://secure2.homedepot.com/MCCCheckout/Checkout/checkoutLogon.do">
+ <input type="hidden" name="dologin" value="yes">
+ <input type="hidden" id="orderId" name="orderId" value="711220840">
+ <h3>I'm a Returning Customer
+ </h3>
+ <div>
+ <span>Your sign in is incorrect. Please enter your email address or password. Note: One more invalid attempt will lock your account.</span>
+ </div>
+ <div>
+ <span>We periodically require password updates. Please <a>reset your password</a> or continue as a guest.</span>
+ </div>
+ <div>
+<span>
+<label>
+<span>Email Address:</span>
+</label>
+ <input type="email" placeholder="you@domain.com" name="logonId" id="email">
+</span>
+ </div>
+ <div>
+<span>
+<label>
+<span>Password:</span>
+</label>
+<label id="toogleBtn">Show</label>
+<input type="password" name="logonPassword" id="password">
+</span>
+</div>
+ <div>
+ <div id="SignInPart">
+ <input id="signInNow" type="submit" value="Sign In">
+ </div>
+ <div>
+ <a>Reset Password</a>
+ </div>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <form>
+ <div role="form" class="tab-target wdk-tab-target wdk-SYF-yes payment-card" id="ITEM_PAYMENT_FLIGHTS_CC" tabindex="-1" style="display: block;"> <section> <fieldset data-di-form-id="payment_line_option_credit_card" data-di-form-track=""> <legend class="invisible-element">Payment line option Credit card</legend> <div> <p id="MCP_CC_CARD_NOT_ELIGIBLE" style="display: none;"><b>Please Note:</b> Not all card types can be used for payment in combination with currency conversion.
+ </p> <div class="cluster cluster-select" id="CLUSTER_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0"><span class=" label " data-di-id="di-id-f9256ae3-51938348"><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0" class=" ">Select your card type</label></span><span class="clearer afterLabel" data-di-id="di-id-71a9adab-a6783585"><span>&nbsp;</span></span><span id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0" class="data select" data-di-field-id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0" data-di-id="#DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0"><div class="sb selectbox data-0 open focused" aria-disabled="false"><div class="display active"><div class="text">- Please select -</div><div class="arrow_btn"><span class="Icon expand-headline normal"><span class="invisible-element">Click to expand</span></span></div></div></div><select id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0" name="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0" aria-required="true" role="listbox" class="has_sb" aria-owns="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_TYPES_0-list" aria-disabled="false" tabindex="0"><option value="NONE" role="option"> Please select </option><option value="CA" role="option">Mastercard</option><option value="VI" role="option">Visa</option><option value="AX" role="option">American Express</option><option value="DC" role="option">Diners Club / Discover</option><option value="JC" role="option">JCB</option><option value="TP" role="option">AirPlus / UATP</option></select></span><span class="clearer" data-di-id="di-id-9c77c574-986cb509"><span>&nbsp;</span></span></div> <hr aria-hidden="true" class="separator separator-small"> <div class="cluster cluster-text partial-border" id="CLUSTER_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0"><span class=" label " data-di-id="di-id-91d0068e-476c9da"><span class=" labelLeft ">Enter your card number<span id="INFO_ICON_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0" class="icon icon-info " data-infotextid="INFO_TEXT_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0" data-proactive="false" data-autohide="true" tabindex="0" data-hasqtip="0"><span class="invisible-element">Information</span></span><div style="display: none;" id="INFO_TEXT_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0"><div id="ALLP.content.Textbox.FlightCcNumber.InfoText" class="dwm-content"><div class="tip-complex tip1 tipbox-4" height="192" width="272">
+ <p><strong>Please make sure you select the correct card type:</strong></p>
+ <br>
+ <strong>American Express</strong> begins with a 3.<br> <strong>Visa</strong> begins with a 4.<br><strong> Mastercard</strong> begins with a 5.<br><strong>Diners Club</strong> begins with a 3 and the second digit must be a 0, 6, or 8. <br><strong>Discover</strong> begins with a 6.<br> <strong>JCB</strong> cards begin with the following four digits: 3088, 3096, 3112, 3158, 3337
+ <br><br>
+ </div></div></div></span></span><span class="clearer afterLabel" data-di-id="di-id-9c77c574-c10145ea"><span>&nbsp;</span></span><span id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0" class="data input card-4" data-di-id="#DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0"><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_00" class="invisible-element">Enter your card number first four digits block</label><input aria-required="true" maxlength="4" style="width: 20%;" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_00" pattern="\d*" type="text" value="" autocomplete="off" aria-disabled="false" tabindex="0" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_00"><span class="separator">|</span><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_01" class="invisible-element">Enter your card number second four digits block</label><input aria-required="true" maxlength="4" style="width: 20%;" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_01" pattern="\d*" type="text" value="" autocomplete="off" aria-disabled="false" tabindex="0" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_01"><span class="separator">|</span><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_02" class="invisible-element">Enter your card number third four digits block</label><input aria-required="true" maxlength="4" style="width: 20%;" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_02" pattern="\d*" type="text" value="" autocomplete="off" aria-disabled="false" tabindex="0" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_02"><span class="separator">|</span><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_03" class="invisible-element">Enter your card number fourth four digits block</label><input aria-required="true" maxlength="4" style="width: 20%;" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_03" pattern="\d*" type="text" value="" autocomplete="off" aria-disabled="false" tabindex="0" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_03"></span><span id="SPINNER_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0" class="field-spinner notdisplayed" data-di-id="#SPINNER_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_NUMBER_0"></span><span class="clearer" data-di-id="di-id-9c77c574-5acbb42e"><span>&nbsp;</span></span></div><div class="cluster cluster-mini cluster-select cluster-dual" id="CLUSTER_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0"><span id="LABEL_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0" class="label " data-di-id="#LABEL_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0"><span class=" labelLeft "><span id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0">Expiry date</span> <small class="info " id="ADDITIONAL_LABEL_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0">(month/year)</small></span></span><span class="clearer" data-di-id="di-id-26bb1425-5b43b2fc"><span>&nbsp;</span></span><span id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0" class="data d-flex select" data-di-field-id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0" data-di-id="#DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0"><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_MONTH_0" class="invisible-element">Expiry date (month/year) Month Selection</label><div class="sb selectbox data-0" aria-disabled="false"><div class="display"><div class="text">01</div><div class="arrow_btn"><span class="Icon expand-headline normal"><span class="invisible-element">Click to expand</span></span></div></div></div><select id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_MONTH_0" data-valuegroup="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0_EXPIRY_DATE" data-valueindex="0" name="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_MONTH_0" aria-required="false" role="listbox" class="has_sb" aria-owns="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_MONTH_0-list" aria-disabled="false" tabindex="0"><option value="01">01</option><option value="02">02</option><option value="03">03</option><option value="04">04</option><option value="05">05</option><option value="06">06</option><option value="07">07</option><option value="08">08</option><option value="09">09</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_YEAR_0" class="invisible-element">Expiry date (month/year) Year Selection</label><div class="sb selectbox data-1" aria-disabled="false"><div class="display"><div class="text">2021</div><div class="arrow_btn"><span class="Icon expand-headline normal"><span class="invisible-element">Click to expand</span></span></div></div></div><select id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_YEAR_0" data-valuegroup="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0_EXPIRY_DATE" data-valueindex="1" name="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_YEAR_0" aria-required="false" role="listbox" class="has_sb" aria-owns="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_EXPIRY_YEAR_0-list" aria-disabled="false" tabindex="0"><option value="21">2021</option><option value="22">2022</option><option value="23">2023</option><option value="24">2024</option><option value="25">2025</option><option value="26">2026</option><option value="27">2027</option><option value="28">2028</option><option value="29">2029</option><option value="30">2030</option><option value="31">2031</option><option value="32">2032</option><option value="33">2033</option><option value="34">2034</option><option value="35">2035</option><option value="36">2036</option><option value="37">2037</option><option value="38">2038</option><option value="39">2039</option></select><input type="hidden" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0_EXPIRY_DATE" value="01/21" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_EXPIRY_DATE_0_EXPIRY_DATE"></span><span class="clearer" data-di-id="di-id-f5261533-b4ef2ed8"><span>&nbsp;</span></span></div><div class="cluster cluster-text cluster-mini" id="CLUSTER_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_0"><span class=" label " data-di-id="di-id-8912f414-6ecc1dca"><span class=" labelLeft ">Security Code <small class="info " id="ADDITIONAL_LABEL_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_0">(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.</small></span></span><span class="clearer afterLabel" data-di-id="di-id-f5261533-5e23f38f"><span>&nbsp;</span></span><span id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_0" class="data input card-1" data-di-id="#DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_0"><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_00" class="invisible-element">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. 4 digits block</label><input aria-required="true" maxlength="4" style="width: 95%;" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_00" type="text" value="" autocomplete="off" aria-disabled="false" tabindex="0" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_CC_SECURITY_CODE_00"></span><span class="clearer" data-di-id="di-id-9f45a8c0-fb4556d"><span>&nbsp;</span></span></div> <div class="wdk-payment-edit"><div class="inline-buttons"> <span class="clearer" data-di-id="di-id-8c6a68d1-4c06ba33"><span>&nbsp;</span></span> </div></div> <div class="cluster cluster-checkbox" id="CLUSTER_ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0"><span id="DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0" class="data checkbox d-flex flex-column" data-di-id="#DATA_ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0"><input aria-required="false" data-onvalue="1" data-offvalue="0" type="checkbox" id="ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0" name="ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0" aria-disabled="false" tabindex="0" data-di-field-id="ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0"></span><label for="ITEM_PAYMENT_FLIGHTS_FLIGHT_REMEMBER_CC_PAYMENT_OPTION_0" class=" label ">Remember this as my preferred payment option for future bookings.</label><span class="clearer" data-di-id="di-id-d9691fec-44549b25"><span>&nbsp;</span></span></div> <leg-miles-and-more-payment ng-version="9.1.13"><miles-and-more-payment-pres _nghost-gpj-c217=""><!----></miles-and-more-payment-pres></leg-miles-and-more-payment> </div> </fieldset> <div class="info"> <hr aria-hidden="true"> <div class="partners 3ds-icons-div"> <p> <span class="ico ico-lock ico-lock-footer-3ds" data-di-id="di-id-6716e5dc-3c143102">&nbsp;</span> <small>Your payment details will be processed applying highest security standards.<br><br>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.</small> </p> <span class="clearer" data-di-id="di-id-ab404bff-9912b7ce"> <span>&nbsp;</span> </span> </div> </div> <span class="clearer" data-di-id="di-id-a6101a37-d8efe855"><span>&nbsp;</span></span> </section>
+ <button type="submit" action="/">Submit form</button>
+ </div>
+</form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" data-page-type="select-pmm-page" data-page-model="one-page" data-shopperlocale="en_GB"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' data:; media-src 'self' data:; style-src 'self' data: 'unsafe-inline'; font-src 'self' data:; frame-src 'self' data:"><meta http-equiv="Memento-Datetime" content="Thu, 04 Jun 2020 09:28:03 GMT"><link rel="original" href="https://live.adyen.com/hpp/pay.shtml">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <title>Step 1: Choose your Payment Method</title>
+
+ <link rel="shortcut icon" href="resources/EN_B357a/1.ico" data-original-href="https://live.adyen.com/sf/q2TgsJh7/img/favicon.ico">
+
+ <link rel="stylesheet" type="text/css" href="resources/EN_B357a/2.css" data-original-href="https://live.adyen.com/hpp/css/reset.css;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e?v=8160">
+ <link rel="stylesheet" media="screen" type="text/css" href="resources/EN_B357a/3.css" data-original-href="https://live.adyen.com/sf/q2TgsJh7/css/screen.css">
+ <link rel="stylesheet" media="print" type="text/css" href="resources/EN_B357a/4.css" data-original-href="https://live.adyen.com/sf/q2TgsJh7/css/print.css">
+
+
+ <!--[if lt IE 7]>
+ <link rel="stylesheet" type="text/css" href="/sf/q2TgsJh7/css/screen_ie6.css" />
+ <![endif]-->
+</head>
+<body>
+
+
+
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
+<meta name="apple-mobile-web-app-capable" content="yes">
+<div class="swatch-black nav desktop-nav-checkout">
+ <div class="desktop-container">
+ <div class="container-padding">
+ <ul class="logo-banner">
+ <li class="logo"></li>
+ </ul>
+ <div class="desktop-checkout-help mobileHide">
+ <p class="colour-black50 size-7 break-2-size-9">Need help?</p>
+ <p class="colour-white size-8 break-2-size-9">Call 01202 668 545</p>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="desktop-container layout-main-content-region mobileHide">
+ <div class="container-padding">
+ <div class="block--commerce-checkout-progress-indication">
+ <ol class="commerce-checkout-progress">
+ <li class="addresses">
+ <span class="checkout-progress-img"></span>
+ <span class="checkout-progress-line"></span>
+ <span class="size-6">Addresses</span>
+ </li>
+ <li class="next summary" title="Review your order before continuing.">
+ <span class="checkout-progress-img"></span>
+ <span class="checkout-progress-line"></span>
+ <span class="size-6">Order summary</span>
+ </li>
+ <li class="active payment" title="Use the button below to proceed to the payment server.">
+ <span class="checkout-progress-img"></span>
+ <span class="checkout-progress-line"></span>
+ <span class="size-6">Payment</span>
+ </li>
+ <li class="complete" style="display:none;">Checkout complete</li>
+ </ol>
+ </div>
+ </div>
+</div>
+<div class="centerpage">
+
+ <form id="pageform" action="https://live.adyen.com/hpp/completeCard.shtml" method="post" autocomplete="off">
+ <div id="content">
+ <!-- <div id="logoheader">
+ <div class="SmartLogo"></div>
+ <div class="IAlogo"></div>
+</div> -->
+
+<div id="logoheader2">
+</div>
+
+<div id="pmcontent" data-fathom="form">
+
+<div class="InfoBox">
+ <div class="Payment"> </div>
+
+ </div>
+ <div class="paddiv1"></div>
+
+
+
+ <input type="hidden" id="displayGroup" name="displayGroup" value="card">
+<h2 id="stageheader">Step 1: Please select your payment method</h2>
+
+<div id="displayAmount">
+ Total payment amount GBP 31.40 <span id="extraCostAmount"></span>
+</div>
+
+
+<ul id="paymentMethods">
+
+
+
+
+ <li style="list-style-type: none;" data-variant="givex">
+ <input type="submit" name="brandName" value="LUSH Gift Card" class="imgB pmB pmBgivex" data-fathom="ccPaymentType">
+ <span id="pmmextracosts-givex" class="pmmextracosts">
+ </span>
+
+ <span id="pmgivexdescription" class="pmmdescription"></span>
+ <div id="pmmdetails-givex" class="pmmdetails" style="overflow: hidden; visibility: visible; height: 0px;">
+
+
+
+
+<br><br>
+<table class="basetable">
+<tbody><tr>
+ <td><div>Name</div></td>
+ <td><div class="fieldDiv"><input type="text" class="inputField" id="givex.cardHolderName" name="givex.cardHolderName" value="" size="19" maxlength="30"></div></td>
+</tr>
+<tr>
+ <td><div>Card Number</div></td>
+ <td>
+ <div class="fieldDiv"><input type="text" class="inputField" id="givex.cardNumber" name="givex.cardNumber" value="" size="22" maxlength="22"></div>
+ </td>
+</tr>
+ <tr>
+ <td colspan="2"><div class="l">
+ <input type="checkbox" value="true" name="givex.partialPayments" id="givex.partialPayments" checked="checked"> If there is not enough balance on my card, pay the rest of the payment amount with an other payment method.
+ </div></td>
+ </tr>
+ <input type="hidden" value="false" name="disablePartialPaymentsInHPP" id="disablePartialPaymentsInHPP">
+ <tr>
+ <td colspan="2"><div class="r">
+ <input class="paySubmit paySubmitgivex" type="submit" name="pay" value="Pay">
+ </div></td>
+ </tr>
+ </tbody></table>
+
+<br><br>
+
+
+
+
+ </div></li><li style="list-style-type: none;" data-variant="amex">
+ <input type="submit" name="brandName" value="Card Payment" class="imgB pmB pmBcard" data-fathom="ccPaymentType">
+ <span id="pmmextracosts-card" class="pmmextracosts">
+ </span>
+
+ <span id="pmcarddescription" class="pmmdescription"></span>
+ <div id="pmmdetails-card" class="pmmdetails" style="overflow: hidden; visibility: visible; height: 475px;" data-collapse="collapsed">
+
+
+
+
+<!-- useNewCardId = true, groupName = card -->
+
+
+
+
+<table class="basetable">
+
+
+
+<tbody><tr id="card.cclogoTr">
+ <td class="mid">
+ <div id="card.cclogoheader" style="display: none">Card Type</div>
+ </td>
+ <td class="mid">
+ <div style="height: 25px" id="card.cclogo">
+ <img alt="" id="card.cclogo0" style="display: inline;" class="mid" src="resources/EN_B357a/5.png" data-original-src="https://live.adyen.com/hpp//img/pm/amex_small.png">
+ <img alt="" id="card.cclogo1" style="display: inline;" class="mid" src="resources/EN_B357a/6.png" data-original-src="https://live.adyen.com/hpp//img/pm/mc_small.png">
+ <img alt="" id="card.cclogo2" style="display: inline;" class="mid" src="resources/EN_B357a/7.png" data-original-src="https://live.adyen.com/hpp//img/pm/visa_small.png">
+ <img alt="" id="card.cclogo3" style="display: none" class="mid" src="resources/EN_B357a/8.png" data-original-src="https://live.adyen.com/hpp/img/pm/unknown_small.png;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e">
+ <img alt="" id="card.cclogo4" style="display: none" class="mid" src="resources/EN_B357a/8.png" data-original-src="https://live.adyen.com/hpp/img/pm/unknown_small.png;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e">
+ <img alt="" id="card.cclogo5" style="display: none" class="mid" src="resources/EN_B357a/8.png" data-original-src="https://live.adyen.com/hpp/img/pm/unknown_small.png;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e">
+ <img alt="" id="card.cclogo6" style="display: none" class="mid" src="resources/EN_B357a/8.png" data-original-src="https://live.adyen.com/hpp/img/pm/unknown_small.png;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e">
+ <img alt="" id="card.cclogo7" style="display: none" class="mid" src="resources/EN_B357a/8.png" data-original-src="https://live.adyen.com/hpp/img/pm/unknown_small.png;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e">
+ <img alt="" id="card.cclogo8" style="display: none" class="mid" src="resources/EN_B357a/8.png" data-original-src="https://live.adyen.com/hpp/img/pm/unknown_small.png;jsessionid=B450B7268B623F8AA545472EE35D974D.live110e">
+
+ </div>
+ </td>
+</tr>
+
+<tr id="card.cardNumberTr">
+ <td class="cardNumberTitle"><div>Card Number</div></td>
+ <td><div class="fieldDiv"><input type="text" class="inputField" id="card.cardNumber" autocomplete="cc-number" name="card.cardNumber" value="" size="24" maxlength="23" data-fathom="number"></div></td>
+</tr>
+
+<tr>
+ <td><div>Card Holder Name</div></td>
+ <td><div class="fieldDiv">
+ <input type="text" class="inputField" id="card.cardHolderName" name="card.cardHolderName" value="" size="19" maxlength="30" data-fathom="name">
+ </div></td>
+</tr>
+
+
+<tr>
+ <td><div>Card Expiry Date</div></td><td>
+ <div class="fieldDiv" id="card.expiryContainer">
+ <select class="inputField hpp-expiry-month" name="card.expiryMonth" id="card.expiryMonth" size="1" data-fathom="expirationMonth">
+ <option value="">&nbsp;</option>
+ <option value="01">01</option>
+ <option value="02">02</option>
+ <option value="03">03</option>
+ <option value="04">04</option>
+ <option value="05">05</option>
+ <option value="06">06</option>
+ <option value="07">07</option>
+ <option value="08">08</option>
+ <option value="09">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ &nbsp;/&nbsp;
+ <select class="inputField hpp-expiry-year" name="card.expiryYear" id="card.expiryYear" size="1" data-fathom="expirationYear">
+ <option value="">&nbsp;</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ <option value="2028">2028</option>
+ <option value="2029">2029</option>
+ <option value="2030">2030</option>
+ <option value="2031">2031</option>
+ <option value="2032">2032</option>
+ <option value="2033">2033</option>
+ <option value="2034">2034</option>
+ <option value="2035">2035</option>
+ <option value="2036">2036</option>
+ <option value="2037">2037</option>
+ <option value="2038">2038</option>
+ <option value="2039">2039</option>
+ <option value="2040">2040</option>
+ <option value="2041">2041</option>
+ <option value="2042">2042</option>
+ <option value="2043">2043</option>
+ <option value="2044">2044</option>
+ </select>
+ </div>
+ </td>
+</tr>
+
+ <tr>
+ <!-- brandCodeUndef -->
+ <td><div id="card.cvcName">CVC/CVV/CID </div></td>
+ <td><div class="fieldDiv"><input class="inputField" type="text" name="card.cvcCode" value="" id="card.cvcCode" size="7" maxlength="3" data-fathom="security"> &nbsp;
+ <a href="https://live.adyen.com/hpp/pay.shtml#">
+ <span id="card.cvcWhatIs">What is CVC/CVV/CID?</span></a></div></td>
+ </tr>
+
+
+
+
+
+
+ <tr>
+ <td colspan="2"><div class="r">
+ <input class="paySubmit paySubmitcard" type="submit" name="pay" value="Pay">
+ </div></td>
+ </tr>
+</tbody></table>
+
+<div class="popupMsg popupMsgOPP " style="display: none;" id="card.cvcFrame">
+ <h3>What is CVC/CVV/CID?</h3>
+ <span style="width: 15px; height: 15px; border: 1px solid black; border-radius: 15px; position: absolute; right: 10px; top: 10px; text-align: center;">x</span>
+ <p>The Card Security Code (CVC/CVV/CID) is an <i>additional</i>
+ three or four digit security code that is printed (not embossed) on the front or the back
+ of your card.</p>
+ <p>The CVC/CVV/CID is an extra security measure to ensure that you are in possession of the card.</p>
+ </div>
+
+
+
+
+
+
+
+
+
+ </div></li><li style="list-style-type: none;" data-variant="paypal">
+ <input type="submit" name="brandName" value="PayPal" class="imgB pmB pmBpaypal" data-fathom="ccPaymentType">
+ <span id="pmmextracosts-paypal" class="pmmextracosts">
+ </span>
+
+ <span id="pmpaypaldescription" class="pmmdescription"></span>
+ <div id="pmmdetails-paypal" class="pmmdetails" style="overflow: hidden; visibility: visible; height: 0px;">
+
+
+
+ <table class="basetable">
+
+ <tbody><tr>
+ <td>
+ Pay using your PayPal account. You will be redirected to the PayPal system to complete the payment.
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2"><div class="r">
+ <input class="paySubmit paySubmitpaypal" type="submit" name="pay" value="Pay">
+ </div></td>
+ </tr>
+</tbody></table>
+ </div>
+ </li>
+
+
+</ul>
+
+
+ <div id="errorFrame" style="display: none;" class="popupMsg errorFrame">
+ <div id="errorFrameValidationErrors">
+ </div>
+ </div>
+ <div id="okFrame" style="display: none;" class="popupMsg okFrame">
+ <div id="okFrameMessages">
+ </div>
+</div>
+
+
+
+<input type="text" style="display: none">
+ <input type="hidden" name="sig" value="DYjKpO5ZYg5WdOnHGxBGZg8cxbY=">
+ <input type="hidden" name="merchantReference" value="UK-LW-18894662">
+ <input type="hidden" name="brandCode" value="brandCodeUndef">
+ <input type="hidden" name="paymentAmount" value="3140">
+ <input type="hidden" name="currencyCode" value="GBP">
+
+ <input type="hidden" name="skinCode" value="q2TgsJh7">
+ <input type="hidden" name="merchantAccount" value="LushCOMUK">
+ <input type="hidden" name="shopperLocale" value="en_GB">
+ <input type="hidden" name="stage" value="pay">
+ <input type="hidden" name="sessionId" value="Kn5uBswMGbS8dZfKIOc5JBomZXXIscszA2tii46aF9A=">
+
+
+ <input type="hidden" name="sessionValidity" value="2020-06-04T12:25:13+01:00">
+
+
+
+ <input type="hidden" name="countryCode" value="US">
+
+ <input type="hidden" name="shopperEmail" value="alabalaa12@yahoo.com">
+ <input type="hidden" name="shopperReference" value="alabalaa12@yahoo.com">
+
+ <input type="hidden" name="recurringContract" value="RECURRING">
+ <input type="hidden" name="resURL" value="https://uk.lush.com/checkout/18894662/payment/return/ryQWZ_B6eE5h1k03sTpHUomc1Rc2NOsjxDuy0zioQNU">
+
+
+
+ <input type="hidden" name="merchantReturnData" value="ryQWZ_B6eE5h1k03sTpHUomc1Rc2NOsjxDuy0zioQNU">
+
+
+
+ <input type="hidden" name="originalSession" value="H4sIAAAAAAAAAJ1WbXPiNhD+Kx5/aucuYDtACDOdKW9NuBCc8Ja5Ozo3wl6wipBcSebia/vfuwJMCNiXaz8wDLuPdp99dlfiL5tyqkeaLMFu2DFJ7fe2hAVICXIy7KMt0jpWjVl5Vk5WJZaoqBSI9awcRBCsRKJnZbdev67Uat6sjMfXwDWGUJGIY5B9ERBmAgP/ctPahlb/N+qsLEEnkuN3+vj06UurBt1q5K6cSzWObydiHbjDwBv46o/nTpI636h4HEwwZSASrmXaFqEhMhmhaR+xuTYuu3HpVhzEJVgzDzLgTethWweNW7AQEjpEo5knjL231yCDiHA9NELhmW3gu4v+00XG2hxdUb6P9ac3XqoP0ZX9crQZBLvkdh+Lb/v3kzv0ChmCxEwky6RAKSr4lDAaUp0i3HM858KpXTiVses1vGrDvXznuA3HeVG9uyaUIZQwMscPcb1fUxIJYSR+QR2TL0BmZH1D6wi/4ybBSEb5si1QYBKYYobd9mQ47A1u8DRhTHyF8B50JEKVnZozEazOrHtOOIcatiN0prRp/U4Z+7/1XywWCg4BlcZeHrhQxpB/MwxxLtU4jQ+eEBjdgExzXHuqxyZUKyY8PTa9Dl1SWoIhYXte1bFG1r3vD7ofrVZ/2rHPwJFIFAyS9RykLwdkbTp0jlJGK18+SLGhuy62m+eoWCCO7eewfu3Wrs8xwW6y+s2RNe3eNEd/D6Y5oN0aZSt0ItAbBZ6iCyo8D5pb4ikst8ZTUH6RZ6jXVe57XVpQqfSeZ0+ROTBGjtyUL+jzroTMxMjhwFBE5NuRawkc1+k1PDSFLlpU6qhDUn9xjysVFUK+7/0I5CS6BgZxJPhecnRWq9dXFcep1t0jmBIBJWxk1hrFOmDNDsXAKd8IGgAmIiW+a92iTzko0/QcDA4PeCWK+5zds7ZXqzjfRU6JfsCNxwtg9x4Vo3Nv6zyoeyDbwwSGrFsY9EeRbkb2UFkRUbcUggokjTXe4oi7xSakVs96IiqC0BpHYN3RUFmfYynCJNC/vyXPmxm9k4w9rkFyYn4RZv3UFthckD9bn83TFuOCF6V0f7gj+H8h4WHnVdrh1mZpYWX7YmV7UJzsUFzVKS5vQ3Qbh30ptms6QEELY+ZCsyfFCLOUW2FKentz27cPxVOUO3BZrBFdou3x2V/PR9Npc1D99JvP6/zd1Vc36DE+6cvprFyjtU0wrt/2SC+c/GL/8y/bjcnWewkAAA==">
+
+
+
+ <input type="hidden" name="billingAddress.street" value="2250 S MOONEY BLVD"> <input type="hidden" name="billingAddress.houseNumberOrName" value=""> <input type="hidden" name="billingAddress.city" value="LAS VEGAS|NV"> <input type="hidden" name="billingAddress.postalCode" value="89169"> <input type="hidden" name="billingAddress.stateOrProvince" value="CA"> <input type="hidden" name="billingAddress.country" value="US">
+ <input type="hidden" name="deliveryAddress.street" value="2250 S MOONEY BLVD"> <input type="hidden" name="deliveryAddress.houseNumberOrName" value=""> <input type="hidden" name="deliveryAddress.city" value="LAS VEGAS|NV"> <input type="hidden" name="deliveryAddress.postalCode" value="89169"> <input type="hidden" name="deliveryAddress.stateOrProvince" value="CA"> <input type="hidden" name="deliveryAddress.country" value="US">
+ <input type="hidden" name="shopper.firstName" value="Isabella"> <input type="hidden" name="shopper.lastName" value="Rohaz"> <input type="hidden" name="shopper.telephoneNumber" value="5597400581">
+
+
+
+ <input type="hidden" name="openinvoicedata.numberOfLines" value="2">
+ <input type="hidden" name="openinvoicedata.line2.itemAmount" value="2640">
+ <input type="hidden" name="openinvoicedata.line2.itemVatPercentage" value="0">
+ <input type="hidden" name="openinvoicedata.line2.currencyCode" value="GBP">
+ <input type="hidden" name="openinvoicedata.line1.numberOfItems" value="1">
+ <input type="hidden" name="openinvoicedata.line2.numberOfItems" value="1">
+ <input type="hidden" name="openinvoicedata.line1.itemVatAmount" value="0">
+ <input type="hidden" name="openinvoicedata.line1.description" value="Honey I Washed The Kids [product]">
+ <input type="hidden" name="openinvoicedata.line2.itemVatAmount" value="0">
+ <input type="hidden" name="openinvoicedata.line2.description" value="International (Courier) [shipping]">
+ <input type="hidden" name="openinvoicedata.line1.itemVatPercentage" value="0">
+ <input type="hidden" name="openinvoicedata.refundDescription" value="Refund to Isabella Rohaz">
+ <input type="hidden" name="openinvoicedata.line1.itemAmount" value="500">
+ <input type="hidden" name="openinvoicedata.line2.vatCategory" value="None">
+ <input type="hidden" name="openinvoicedata.line1.vatCategory" value="None">
+ <input type="hidden" name="merchantIntegration.type" value="HPP">
+ <input type="hidden" name="openinvoicedata.line1.currencyCode" value="GBP">
+
+
+ <input type="hidden" name="referrerURL" value="https://uk.lush.com/checkout/18894662/payment">
+
+
+
+
+
+
+ <div id="df_swf_c" style="display:none;"></div>
+ <input type="hidden" name="dfValue" id="dfValue" value="1B2M2Y8Asg0010000000000000CEENawQ48p0050271576cVB94iKzBGs1JtJP2ftZ1B2M2Y8Asg000joETVQLPRH00000qZkTE1BXw4QyCgloH7n6Gldz2JXe0J:40">
+
+ <input type="hidden" name="usingFrame" id="usingFrame" value="false">
+ <input type="hidden" name="usingPopUp" id="usingPopUp" value="false">
+
+ <div class="paddiv2"></div>
+ </div>
+ </div>
+ <div id="foot">
+ <div id="footc">
+ <div id="nextstep">
+ <div id="nextstepc">Next Step: Enter your Payment Details</div>
+ </div>
+ <div id="footerb2div">
+ </div>
+ <div id="footerb1div">
+ <input name="back" id="mainBack" value="Previous" type="submit" class="hideforprint footerB backB">
+ </div>
+ </div>
+ </div>
+
+ <input type="hidden" name="shopperBehaviorLog" id="hpp-sbLog" value="{&quot;numberBind&quot;:&quot;1&quot;,&quot;holderNameBind&quot;:&quot;1&quot;,&quot;cvcBind&quot;:&quot;1&quot;,&quot;deactivate&quot;:&quot;11&quot;,&quot;activate&quot;:&quot;10&quot;,&quot;numberFieldFocusCount&quot;:&quot;2&quot;,&quot;numberFieldLog&quot;:&quot;fo@533,bl@544,fo@662,bl@662&quot;,&quot;numberFieldBlurCount&quot;:&quot;2&quot;,&quot;holderNameFieldFocusCount&quot;:&quot;2&quot;,&quot;holderNameFieldLog&quot;:&quot;fo@662,bl@675,fo@793,bl@793&quot;,&quot;holderNameFieldBlurCount&quot;:&quot;2&quot;,&quot;cvcFieldFocusCount&quot;:&quot;2&quot;,&quot;cvcFieldLog&quot;:&quot;fo@1136,bl@1149,fo@1288,bl@1288&quot;,&quot;cvcFieldBlurCount&quot;:&quot;2&quot;}">
+
+ </form>
+ </div>
+
+
+
+
+
+
+</body></html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Macy's Checkout</title>
+ <meta http-equiv="generator" content="JACPKMALPHTCSJDTCR">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="format-detection" content="telephone=no">
+ </head>
+ <body>
+ <form id="rc-payment-info-form" novalidate="">
+ <div>
+ <div>
+ </div>
+ </div>
+ <div id="rc-payment-selection-row">
+ <div>
+ <input type="radio" id="rc-creditcard" for="rc-creditcard-label" name="payment.type" checked="checked" value="CREDITCARD"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Credit card
+ parseable name: payment.type
+ field signature: 2449554739
+ form signature: 4053649612452005841"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label id="rc-creditcard-label" for="rc-creditcard">Credit card</label>
+ </div>
+ <div>
+ <input type="radio" id="rc-paypal" name="payment.type" value="PAYPAL"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Choose payment method
+ parseable name: payment.type
+ field signature: 2449554739
+ form signature: 4053649612452005841"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <span for="rc-paypal">
+</span>
+ </div>
+ </div>
+ <div id="rc-paypal-disclaimer-cc-row">
+ <div id="rc-paypal-disclaimer">
+ <b>Note:&nbsp;</b>PayPal can't be used with Gift Cards, Reward Cards and Credit Cards.
+ Plenti points can be earned but not used with PayPal.
+ </div>
+ </div>
+ <fieldset id="rc-credit-card-container">
+ <div>
+ <div>
+ <div>
+</div>
+ </div>
+ <div>
+ <p>Secure payment
+ <a target="_blank">more info</a>
+ </p>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-card-type">Card type</label>
+ <select name="creditCard.cardType.code" id="rc-payment-card-type" autocomplete="off"
+title="overall type: CREDIT_CARD_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_TYPE
+ label: Card type
+ parseable name: creditCard.cardType.code
+ field signature: 1958753038
+ form signature: 4053649612452005841"
+autofill-prediction="CREDIT_CARD_TYPE"
+>
+ <option value="-1">Select</option>
+ <option value="Y">Macy's</option>
+ <option value="B">Macy's American Express</option>
+ <option value="A">American Express</option>
+ <option value="V">Visa</option>
+ <option value="M">MasterCard</option>
+ <option value="O">Discover</option>
+ <option value="F">Employee Card</option>
+ </select>
+ <div id="payment-aria-info" tabindex="-1">Your Shipping, Plenti, and Gift Card information can be found and verified at the top of this page"</div>
+ </div>
+ </div>
+ <div id="rc-payment-card-number-row">
+ <div>
+ <label for="rc-payment-card-number">Card number</label>
+ <input type="text" maxlength="20" pattern="\d*" name="creditCard.cardNumber" id="rc-payment-card-number" value="" autocomplete="off" autocorrect="off"
+title="overall type: CREDIT_CARD_NUMBER
+ server type: CREDIT_CARD_NUMBER
+ heuristic type: CREDIT_CARD_NUMBER
+ label: Card number
+ parseable name: creditCard.cardNumber
+ field signature: 2117159926
+ form signature: 4053649612452005841"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ <input type="hidden" name="creditCard.maskedCreditCardNumber" value="">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label id="rc-payment-expiration-label">Expiration date</label>
+ </div>
+ </div>
+ <div>
+ <div>
+ <select name="creditCard.expMonth" id="rc-payment-card-month" autocomplete="off"
+title="overall type: CREDIT_CARD_EXP_MONTH
+ server type: CREDIT_CARD_EXP_MONTH
+ heuristic type: CREDIT_CARD_EXP_MONTH
+ label: Expiration date
+ parseable name: creditCard.expMonth
+ field signature: 989675451
+ form signature: 4053649612452005841"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option>01</option>
+ <option>02</option>
+ <option>03</option>
+ <option>04</option>
+ <option>05</option>
+ <option>06</option>
+ <option>07</option>
+ <option>08</option>
+ <option>09</option>
+ <option>10</option>
+ <option>11</option>
+ <option>12</option>
+ </select>
+ </div>
+ <div>
+ <select name="creditCard.expYear" id="rc-payment-card-year" autocomplete="off"
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ server type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ heuristic type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ label: Expiration date
+ parseable name: creditCard.expYear
+ field signature: 891328465
+ form signature: 4053649612452005841"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option>2017</option>
+ <option>2018</option>
+ <option>2019</option>
+ <option>2020</option>
+ <option>2021</option>
+ <option>2022</option>
+ <option>2023</option>
+ <option>2024</option>
+ <option>2025</option>
+ <option>2026</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-scode" id="rc-payment-scode-label">Security&nbsp;code</label>
+ </div>
+ </div>
+ <div id="rc-payment-scode-row">
+ <div>
+ <input type="text" name="fake-password" id="rc-fake-password" autocomplete="off"
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: Please enter the 4 digit security code on the front of your credit card Please enter the 3 digit sec
+ parseable name: fake-password
+ field signature: 3761992124
+ form signature: 4053649612452005841"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ <input type="text" maxlength="4" pattern="\d*" name="creditCard.securityCode" id="rc-payment-scode" autocomplete="off"
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: Security code
+ parseable name: creditCard.securityCode
+ field signature: 789650537
+ form signature: 4053649612452005841"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ </div>
+ <div id="amex-cvv-info" tabindex="-1">Please enter the 4 digit security code on the front of your credit card</div>
+ <div id="not-amex-cvv-info" tabindex="-1">Please enter the 3 digit security code on the back of your credit card</div>
+ <div id="rc-cvv-icon-row">
+ <div id="rc-cvv-icon">
+ <span id="rc-cvv-img">
+</span>
+ <span id="rc-cvv-hint">
+</span>
+ </div>
+ </div>
+ </div>
+ </fieldset>
+ <section id="rc-paypal-container">
+ <div>
+ <div id="rc-paypal-disclaimer">
+ <b>Note:&nbsp;</b>PayPal can't be used with Gift Cards, Reward Cards and Credit Cards.
+ Plenti points can be earned but not used with PayPal.
+ </div>
+ </div>
+ <div id="rc-paypal-login-disclaimer">
+ <div>
+ 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
+ </div>
+ </div>
+ </section>
+ <fieldset id="rc-billing-address-container">
+ <div>
+ <div>
+ </div>
+ </div>
+ <div id="rc-use-shipping-container">
+ <div>
+ <input type="checkbox" id="rc-use-shipping" name="useMyShippingAddress" value="" checked=""
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Use my shipping address
+ parseable name: useMyShippingAddress
+ field signature: 1490259836
+ form signature: 4053649612452005841"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <div>
+ <label id="rc-use-shipping-label" for="rc-use-shipping">Use my shipping address</label>
+ </div>
+ </div>
+ <div id="co-useshippingAddress-summary">
+ <div>
+ </div>
+ </div>
+ <div id="rc-billing-address">
+ <div>
+ <div>
+ <label for="rc-payment-firstName">First name</label>
+ <input type="text" maxlength="20" name="billingContact.firstName" id="rc-payment-firstName" value="" autocorrect="off" autocomplete="given-name"
+title="overall type: HTML_TYPE_GIVEN_NAME
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: First name
+ parseable name: billingContact.firstName
+ field signature: 1110479879
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_GIVEN_NAME"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-lastName">Last name</label>
+ <input type="text" maxlength="30" name="billingContact.lastName" id="rc-payment-lastName" value="" autocorrect="off" autocomplete="family-name"
+title="overall type: HTML_TYPE_FAMILY_NAME
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: Last name
+ parseable name: billingContact.lastName
+ field signature: 724121833
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_FAMILY_NAME"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-line1">Address line 1</label>
+ <input type="text" maxlength="35" name="billingAddress.addressLine1" id="rc-payment-line1" value="" autocorrect="off" autocomplete="address-line1"
+title="overall type: HTML_TYPE_ADDRESS_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address line 1
+ parseable name: billingAddress.addressLine1
+ field signature: 3436138513
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_ADDRESS_LINE1"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-line2">Address line 2 (optional)</label>
+ <input type="text" maxlength="35" name="billingAddress.addressLine2" id="rc-payment-line2" autocorrect="off" autocomplete="address-line2" value=""
+title="overall type: HTML_TYPE_ADDRESS_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address line 2 (optional)
+ parseable name: billingAddress.addressLine2
+ field signature: 3817149136
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_ADDRESS_LINE2"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-city">City</label>
+ <input type="text" maxlength="25" name="billingAddress.city" id="rc-payment-city" value="" autocorrect="off" autocomplete="address-level2"
+title="overall type: HTML_TYPE_ADDRESS_LEVEL2
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: City
+ parseable name: billingAddress.city
+ field signature: 2433902552
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_ADDRESS_LEVEL2"
+>
+ </div>
+ </div>
+ <div id="rc-paystateZipRow">
+ <div>
+ <label for="rc-payment-state">State</label>
+ <select name="billingAddress.state" id="rc-payment-state" autocomplete="address-level1"
+title="overall type: HTML_TYPE_ADDRESS_LEVEL1
+ server type: ADDRESS_HOME_STATE
+ heuristic type: ADDRESS_HOME_STATE
+ label: State
+ parseable name: billingAddress.state
+ field signature: 333754698
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_ADDRESS_LEVEL1"
+>
+ <option value="-1">Select</option>
+ <option value="AL">AL</option>
+ <option value="AK">AK</option>
+ <option value="AS">AS</option>
+ <option value="AZ">AZ</option>
+ <option value="AR">AR</option>
+ <option value="AA">AA</option>
+ <option value="AE">AE</option>
+ <option value="AP">AP</option>
+ <option value="CA">CA</option>
+ <option value="CO">CO</option>
+ <option value="CT">CT</option>
+ <option value="DE">DE</option>
+ <option value="DC">DC</option>
+ <option value="FM">FM</option>
+ <option value="FL">FL</option>
+ <option value="GA">GA</option>
+ <option value="GU">GU</option>
+ <option value="HI">HI</option>
+ <option value="ID">ID</option>
+ <option value="IL">IL</option>
+ <option value="IN">IN</option>
+ <option value="IA">IA</option>
+ <option value="KS">KS</option>
+ <option value="KY">KY</option>
+ <option value="LA">LA</option>
+ <option value="ME">ME</option>
+ <option value="MH">MH</option>
+ <option value="MD">MD</option>
+ <option value="MA">MA</option>
+ <option value="MI">MI</option>
+ <option value="MN">MN</option>
+ <option value="MS">MS</option>
+ <option value="MO">MO</option>
+ <option value="MT">MT</option>
+ <option value="NE">NE</option>
+ <option value="NV">NV</option>
+ <option value="NH">NH</option>
+ <option value="NJ">NJ</option>
+ <option value="NM">NM</option>
+ <option value="NY">NY</option>
+ <option value="NC">NC</option>
+ <option value="ND">ND</option>
+ <option value="MP">MP</option>
+ <option value="OH">OH</option>
+ <option value="OK">OK</option>
+ <option value="OR">OR</option>
+ <option value="PW">PW</option>
+ <option value="PA">PA</option>
+ <option value="PR">PR</option>
+ <option value="RI">RI</option>
+ <option value="SC">SC</option>
+ <option value="SD">SD</option>
+ <option value="TN">TN</option>
+ <option value="TX">TX</option>
+ <option value="VI">VI</option>
+ <option value="UT">UT</option>
+ <option value="VT">VT</option>
+ <option value="VA">VA</option>
+ <option value="WA">WA</option>
+ <option value="WV">WV</option>
+ <option value="WI">WI</option>
+ <option value="WY">WY</option>
+ </select>
+ </div>
+ <div>
+ <label for="rc-payment-zipCode">ZIP code</label>
+ <input type="text" maxlength="5" pattern="\d*" name="billingAddress.zipCode" id="rc-payment-zipCode" value="" autocorrect="off" autocomplete="postal-code"
+title="overall type: HTML_TYPE_POSTAL_CODE
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP code
+ parseable name: billingAddress.zipCode
+ field signature: 837276327
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_POSTAL_CODE"
+>
+ </div>
+ </div>
+ </div>
+ </fieldset>
+ <div>
+ <div>
+ </div>
+ </div>
+ <div>
+ <div>
+ We'll only contact you if we have questions about this order.
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-phone">Phone number</label>
+ <input type="text" maxlength="14" pattern="\d*" name="billingAddress.phone" id="rc-payment-phone" value="" autocorrect="off" autocomplete="tel"
+title="overall type: HTML_TYPE_TEL
+ server type: PHONE_HOME_CITY_AND_NUMBER
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: Phone number
+ parseable name: billingAddress.phone
+ field signature: 2467847771
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_TEL"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-payment-email">Email address</label>
+ <input type="email" maxlength="75" name="billingContact.email" id="rc-payment-email" value="" autocapitalize="off" autocorrect="off" autocomplete="email"
+title="overall type: HTML_TYPE_EMAIL
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: Email address
+ parseable name: billingContact.email
+ field signature: 4157735572
+ form signature: 4053649612452005841"
+autofill-prediction="HTML_TYPE_EMAIL"
+>
+ </div>
+ </div>
+ <fieldset>
+ <div id="rc-paypalcontinue-row">
+ <div>
+ <button type="button" id="rc-paypal-continue">Continue to Paypal</button>
+ </div>
+ </div>
+ <div id="rc-normalpaycontinue-row">
+ <div>
+ <button type="submit" id="rc-payment-continue">Continue</button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+ <input type="hidden" id="gmeUrl" value="https://www.googleapis.com/mapsengine/v1/tables/06739517320133004748-11853667273131550346/features?version=published&amp;key=AIzaSyCzwiHW1tSp_4FXaFuORRffbxBzQUN1qs4">
+ <input type="hidden" id="gmClientId" value="gme-macysinc">
+ <input type="hidden" id="gmeAPIKey" value="AIzaSyCzwiHW1tSp_4FXaFuORRffbxBzQUN1qs4">
+ <input type="hidden" id="gmeTableId" value="06739517320133004748-11853667273131550346">
+ <input type="hidden" id="gmeToSdpEnabled" value="true">
+ <input type="hidden" id="macysCookieDomain" value=".macys.com">
+ <input type="hidden" id="MACYS_secureHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_baseHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_assetsHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_imageHostName" value="/img/ts/is/image/MCY">
+ <input type="hidden" id="AKAMAI_LOGIC" value="hybrid">
+ <input type="hidden" id="searchBoxPlaceholderGuest" value="Search or enter web ID">
+ <input type="hidden" id="searchBoxPlaceholderUser" value="[userName], Search or enter web ID">
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Macy's Checkout</title>
+ <meta http-equiv="generator" content="JACPKMALPHTCSJDTCR">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="format-detection" content="telephone=no">
+ <meta name="viewport" content="width=device-width">
+ </head>
+ <body>
+ <form id="rc-shipping-info-form">
+ <fieldset>
+ <fieldset id="rc-shipping-address-wrapper">
+ <div>
+ <div>
+ <label for="rc-shipping-firstName">First name</label>
+ <input type="text" maxlength="20" name="contact.firstName" id="rc-shipping-firstName" value="" autocorrect="off" autocomplete="given-name"
+title="overall type: HTML_TYPE_GIVEN_NAME
+ server type: NO_SERVER_DATA
+ heuristic type: NAME_FIRST
+ label: First name
+ parseable name: contact.firstName
+ field signature: 2682246885
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_GIVEN_NAME"
+>
+<small>Please enter a first name.</small>
+ <div>
+ <div>
+</div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-shipping-lastName">Last name</label>
+ <input type="text" maxlength="30" name="contact.lastName" id="rc-shipping-lastName" value="" autocorrect="off" autocomplete="family-name"
+title="overall type: HTML_TYPE_FAMILY_NAME
+ server type: NO_SERVER_DATA
+ heuristic type: NAME_LAST
+ label: Last name
+ parseable name: contact.lastName
+ field signature: 4271897545
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_FAMILY_NAME"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-shipping-line1">Address line 1</label>
+ <input type="text" maxlength="35" name="address.addressLine1" id="rc-shipping-line1" value="" autocorrect="off" autocomplete="address-line1"
+title="overall type: HTML_TYPE_ADDRESS_LINE1
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address line 1
+ parseable name: address.addressLine1
+ field signature: 2758559035
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_ADDRESS_LINE1"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-shipping-line2">Address line 2 (optional)</label>
+ <input type="text" maxlength="35" name="address.addressLine2" id="rc-shipping-line2" value="" autocorrect="off" autocomplete="address-line2" placeholder="Apt, Suite, Bldg, Floor, etc"
+title="overall type: HTML_TYPE_ADDRESS_LINE2
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address line 2 (optional)
+ parseable name: address.addressLine2
+ field signature: 1355777065
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_ADDRESS_LINE2"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-shipping-city">City</label>
+ <input type="text" maxlength="25" name="address.city" id="rc-shipping-city" value="" autocorrect="off" autocomplete="address-level2"
+title="overall type: HTML_TYPE_ADDRESS_LEVEL2
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_CITY
+ label: City
+ parseable name: address.city
+ field signature: 242591127
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_ADDRESS_LEVEL2"
+>
+ </div>
+ </div>
+ <div id="rc-stateZipRow">
+ <div>
+ <label for="rc-shipping-state">State</label>
+ <select name="address.state" id="rc-shipping-state" autocomplete="address-level1"
+title="overall type: HTML_TYPE_ADDRESS_LEVEL1
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_STATE
+ label: State
+ parseable name: address.state
+ field signature: 1305630158
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_ADDRESS_LEVEL1"
+>
+ <option value="-1">Select</option>
+ <option value="AL">AL</option>
+ <option value="AK">AK</option>
+ <option value="AS">AS</option>
+ <option value="AZ">AZ</option>
+ <option value="AR">AR</option>
+ <option value="AA">AA</option>
+ <option value="AE">AE</option>
+ <option value="AP">AP</option>
+ <option value="CA">CA</option>
+ <option value="CO">CO</option>
+ <option value="CT">CT</option>
+ <option value="DE">DE</option>
+ <option value="DC">DC</option>
+ <option value="FM">FM</option>
+ <option value="FL">FL</option>
+ <option value="GA">GA</option>
+ <option value="GU">GU</option>
+ <option value="HI">HI</option>
+ <option value="ID">ID</option>
+ <option value="IL">IL</option>
+ <option value="IN">IN</option>
+ <option value="IA">IA</option>
+ <option value="KS">KS</option>
+ <option value="KY">KY</option>
+ <option value="LA">LA</option>
+ <option value="ME">ME</option>
+ <option value="MH">MH</option>
+ <option value="MD">MD</option>
+ <option value="MA">MA</option>
+ <option value="MI">MI</option>
+ <option value="MN">MN</option>
+ <option value="MS">MS</option>
+ <option value="MO">MO</option>
+ <option value="MT">MT</option>
+ <option value="NE">NE</option>
+ <option value="NV">NV</option>
+ <option value="NH">NH</option>
+ <option value="NJ">NJ</option>
+ <option value="NM">NM</option>
+ <option value="NY">NY</option>
+ <option value="NC">NC</option>
+ <option value="ND">ND</option>
+ <option value="MP">MP</option>
+ <option value="OH">OH</option>
+ <option value="OK">OK</option>
+ <option value="OR">OR</option>
+ <option value="PW">PW</option>
+ <option value="PA">PA</option>
+ <option value="PR">PR</option>
+ <option value="RI">RI</option>
+ <option value="SC">SC</option>
+ <option value="SD">SD</option>
+ <option value="TN">TN</option>
+ <option value="TX">TX</option>
+ <option value="VI">VI</option>
+ <option value="UT">UT</option>
+ <option value="VT">VT</option>
+ <option value="VA">VA</option>
+ <option value="WA">WA</option>
+ <option value="WV">WV</option>
+ <option value="WI">WI</option>
+ <option value="WY">WY</option>
+ </select>
+ </div>
+ <div>
+ <label for="rc-shipping-postal-code">ZIP code</label>
+ <input type="text" maxlength="5" name="address.zipCode" id="rc-shipping-postal-code" value="" autocorrect="off" autocomplete="postal-code" pattern="\d*"
+title="overall type: HTML_TYPE_POSTAL_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: ADDRESS_HOME_ZIP
+ label: ZIP code
+ parseable name: address.zipCode
+ field signature: 1541909938
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_POSTAL_CODE"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-shipping-phone">Phone number</label>
+ <input type="text" maxlength="14" pattern="\d*" name="address.phone" id="rc-shipping-phone" autocomplete="tel" value="" autocorrect="off"
+title="overall type: HTML_TYPE_TEL
+ server type: NO_SERVER_DATA
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: Phone number
+ parseable name: address.phone
+ field signature: 3405332267
+ form signature: 6571413743856647727"
+autofill-prediction="HTML_TYPE_TEL"
+>
+ </div>
+ </div>
+ </fieldset>
+ </fieldset>
+ <fieldset id="rc-shipping-method-set">
+ <div>
+ <div>
+ <legend tabindex="-1">Shipping method</legend>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <input type="radio" name="shippingMethod.methodCode" value="G" id="rc-shipping-shipping-methodG" checked="checked"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Standard Transit time: 3-6 business days
+ parseable name: shippingMethod.methodCode
+ field signature: 1185913307
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <div>
+ <label for="rc-shipping-shipping-methodG">Standard
+ <br>
+ Transit time: 3-6 business days
+ </label>
+ </div>
+ <div>
+ <div>$10.95
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="radio" name="shippingMethod.methodCode" value="2" id="rc-shipping-shipping-method2" disabled=""
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Premium Transit time: 2-3 business days
+ parseable name: shippingMethod.methodCode
+ field signature: 1185913307
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <div>
+ <label for="rc-shipping-shipping-method2">Premium
+ <br>
+ Transit time: 2-3 business days
+ </label>
+ </div>
+ <div>
+ <div>$19.95
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="radio" name="shippingMethod.methodCode" value="O" id="rc-shipping-shipping-methodO" disabled=""
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Express Transit time: 1-2 business days
+ parseable name: shippingMethod.methodCode
+ field signature: 1185913307
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <div>
+ <label for="rc-shipping-shipping-methodO">Express
+ <br>
+ Transit time: 1-2 business days
+ </label>
+ </div>
+ <div>
+ <div>$29.95
+ </div>
+ </div>
+ </div>
+ <div>
+ <p>
+<b>Note: We'll send you an email to schedule your delivery.</b>
+</p>
+ <p>
+<b>Note: Some items in your order may ship separately. Transit time is the time between leaving our fulfillment center &amp; delivery to you.</b>
+</p>
+ </div>
+ </div>
+ </fieldset>
+ <fieldset>
+ <div>
+ <div>
+ <legend tabindex="-1">
+ Gift Options
+ </legend>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="checkbox" name="giftOrder" id="rc-giftoption-isgift" value="true"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: This order contains a gift
+ parseable name: giftOrder
+ field signature: 2509042763
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <div id="aria-giftcardinfo" tabindex="-1">
+ "Selecting this checkbox will expand additional gift options"
+ </div>
+ <label for="rc-giftoption-isgift">This order contains a gift</label>
+ <input type="hidden" value="true" name="_giftOrder">
+ </div>
+ </div>
+ <div id="rc-giftoptions-additional">
+ <div>
+ <div>
+ <input type="checkbox" name="giftOptions.haveGiftMessage" value="true" id="rc-giftoption-giftMsg"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Gift message (optional)
+ parseable name: giftOptions.haveGiftMessage
+ field signature: 1542269083
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="rc-giftoption-giftMsg">Gift message (optional)</label>
+ <input type="hidden" value="true" name="_giftOptions.haveGiftMessage">
+<br>
+ </div>
+ </div>
+ <div id="rc-giftmsg-text">
+ <div>
+ <div>
+ <span id="rc-giftmsg-additional-info">Write a personal message. We'll print it on a card &amp; send it along with the order.</span>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-gift-msg1">Message line 1 (max 45 characters)</label>
+ <input type="text" maxlength="45" name="giftOptions.senderName" id="rc-gift-msg1" value="" autocomplete="off" placeholder="Message line 1 (max 45 characters)"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Message line 1 (max 45 characters)
+ parseable name: giftOptions.senderName
+ field signature: 851756973
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-gift-msg2">Message line 2 (max 45 characters)</label>
+ <input type="text" maxlength="45" name="giftOptions.giftMessage1" id="rc-gift-msg2" value="" autocomplete="off" placeholder="Message line 2 (max 45 characters)"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Message line 2 (max 45 characters)
+ parseable name: giftOptions.giftMessage1
+ field signature: 893448548
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="rc-gift-msg3">Message line 3 (max 45 characters)</label>
+ <input type="text" maxlength="45" name="giftOptions.giftMessage2" id="rc-gift-msg3" value="" autocomplete="off" placeholder="Message line 3 (max 45 characters)"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Message line 3 (max 45 characters)
+ parseable name: giftOptions.giftMessage2
+ field signature: 3147898435
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="checkbox" name="giftOptions.packageInGiftBox" value="true" id="rc-giftoption-giftBox"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Send in a gift box ($6.00 per order)
+ parseable name: giftOptions.packageInGiftBox
+ field signature: 1556860808
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="rc-giftoption-giftBox">Send in a gift box ($6.00 per order)</label>
+ <input type="hidden" value="true" name="_giftOptions.packageInGiftBox">
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="checkbox" name="giftOptions.sendGiftReceipt" value="true" id="rc-giftoption-packingSlip" checked="checked"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Hide prices on the packing slip
+ parseable name: giftOptions.sendGiftReceipt
+ field signature: 367416642
+ form signature: 6571413743856647727"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="rc-giftoption-packingSlip">Hide prices on the packing slip</label>
+ <input type="hidden" value="true" name="_giftOptions.sendGiftReceipt">
+ </div>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+ <input type="hidden" id="gmeUrl" value="https://www.googleapis.com/mapsengine/v1/tables/06739517320133004748-11853667273131550346/features?version=published&amp;key=AIzaSyCzwiHW1tSp_4FXaFuORRffbxBzQUN1qs4">
+ <input type="hidden" id="gmClientId" value="gme-macysinc">
+ <input type="hidden" id="gmeAPIKey" value="AIzaSyCzwiHW1tSp_4FXaFuORRffbxBzQUN1qs4">
+ <input type="hidden" id="gmeTableId" value="06739517320133004748-11853667273131550346">
+ <input type="hidden" id="gmeToSdpEnabled" value="true">
+ <input type="hidden" id="macysCookieDomain" value=".macys.com">
+ <input type="hidden" id="MACYS_secureHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_baseHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_assetsHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_imageHostName" value="/img/ts/is/image/MCY">
+ <input type="hidden" id="AKAMAI_LOGIC" value="hybrid">
+ <input type="hidden" id="searchBoxPlaceholderGuest" value="Search or enter web ID">
+ <input type="hidden" id="searchBoxPlaceholderUser" value="[userName], Search or enter web ID">
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en" id="yui_3_8_1_1_1489978541398_168">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
+ <title>Sign In - Macy's Checkout</title>
+ <meta http-equiv="generator" content="JACPKMALPHTCSJDTCR">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="format-detection" content="telephone=no">
+ <meta name="viewport" content="width=device-width">
+ </head>
+ <body id="yui_3_8_1_1_1489978541398_167">
+ <form id="signInForm" action="https://www.macys.com/account/signin?fromCheckout=fromCheckout" method="post">
+ <div id="yui_3_8_1_1_1489978541398_165">
+ <ul id="yui_3_8_1_1_1489978541398_164">
+ <li id="yui_3_8_1_1_1489978541398_163">
+ <div>
+ <label for="emailAddr">Email address:</label>
+ </div>
+ <div id="yui_3_8_1_1_1489978541398_162">
+ <input id="emailAddr" name="email" type="text" value="" maxlength="75">
+ </div>
+ <div>&nbsp;</div>
+ </li>
+ <li id="yui_3_8_1_1_1489978541398_175">
+ <div>
+ <label for="password">
+ Password:
+ </label>
+ </div>
+ <div id="yui_3_8_1_1_1489978541398_174">
+ <input id="password" name="password" type="password" value="" maxlength="16">
+ </div>
+ <div>&nbsp;</div>
+ </li>
+ </ul>
+ <div>
+ <div>
+<span>Password is case sensitive</span>
+</div>
+ <div>
+ <a id="frgtPwd">Forgot Your Password?</a>
+ </div>
+ <div>
+ <button type="submit" id="isnormalcheckout" name="accountSignIn">
+<span>checkout</span>
+</button>
+ </div>
+ </div>
+ </div>
+ </form>
+ <form id="emailForm" action="https://www.macys.com/account/signin?fromCheckout=fromCheckout" method="post">
+ <fieldset>
+ <ul>
+ <li id="overlaySubmitDiv">
+ <label name="" for="forgotEmail" csserrorclass="error">
+ Get started by entering the email address you use to sign in:
+ </label>
+ <input id="forgotEmail" name="passwordRecovery.email" type="text" value="">
+ </li>
+ <li id="overlaySubmitbtnDiv">
+ <button id="verifySubmitBtn" type="submit">
+<span>continue </span>
+</button>
+ </li>
+ </ul>
+ </fieldset>
+ </form>
+ <form id="capthaForm" action="https://www.macys.com/account/signin?fromCheckout=fromCheckout" method="post">
+ <fieldset>
+ <ul>
+ <li>
+ <label name="email" for="email" csserrorclass="error">
+ Your email address:
+ </label>
+ <span>
+<span id="emailID">
+</span>
+</span>
+ </li>
+ <li id="Captchapart">
+ <img id="captchaImg" alt="enter the letters in the below field">
+ <div>
+<a>
+<img id="refreshbtn" src="./Sign In - Macy&#39;s Checkout_files/new_image.gif" alt="new_image">
+</a>
+</div>
+ </li>
+ <li>
+ <label path="securityCode" for="securityCode" csserrorclass="error">
+ Please enter the characters shown above and we'll email you a link to reset your password:
+ </label>
+ <input id="securityCode" name="passwordRecovery.securityCode" type="text" value="" maxlength="50">
+ <input id="refreshCaptcha" name="passwordRecovery.refreshCaptcha" type="hidden" value="">
+ </li>
+ <li>
+ <input id="verifyCaptchaBtn" type="submit" value="submit">
+ </li>
+ </ul>
+ </fieldset>
+ </form>
+ <form id="securityQAForm" action="https://www.macys.com/account/signin?fromCheckout=fromCheckout" method="post">
+ <fieldset>
+ <ul>
+ <li>
+ <label name="email" for="email" csserrorclass="error">
+ Your email address:
+ <span>
+ <div id="secureEmailID">
+</div>
+ </span>
+ </label>
+ <input id="hiddenEmail" name="passwordRecovery.email" type="hidden" value="">
+ <input id="hiddenSecQue" name="passwordRecovery.question" type="hidden" value="">
+ </li>
+ <li>
+ <div id="question">
+</div>
+ </li>
+ <li>
+ <input id="secureAnswer" name="passwordRecovery.secureAnswer" type="text" value="">
+ </li>
+ <li>
+ <button type="submit" id="verifysecurityBtn">
+<span>continue </span>
+</button>
+ </li>
+ </ul>
+ </fieldset>
+ </form>
+ <form id="resetPasswordForm" action="https://www.macys.com/account/signin?fromCheckout=fromCheckout" method="post">
+ <fieldset>
+ <ul>
+ <li>
+ <div>
+ <div>
+ <label name="password" for="passwordfield" csserrorclass="error inline" id="resetPasswordLabel">
+ Enter new password: </label>
+ </div>
+ <div>
+ <div>
+ <input id="passwordfield" name="passwordRecovery.password"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Enter new password:
+ parseable name: passwordRecovery.password
+ field signature: 457433414
+ form signature: 10250648745005274367" type="password" value="" maxlength="16"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <div>
+ <div>
+</div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li>
+ <div>
+ <div>
+ <label path="verifyPassword" for="verifyPassword" csserrorclass="error inline" id="confirmPasswordLabel">
+ Confirm password:
+ </label>
+ </div>
+ <div>
+ <div>
+ <input id="verifyPasswordfield" name="passwordRecovery.verifyPassword"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Confirm password:
+ parseable name: passwordRecovery.verifyPassword
+ field signature: 1462365960
+ form signature: 10250648745005274367" type="password" value="" maxlength="16"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ <div>
+ <div>
+</div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li>
+ <button id="verifyResetPasswordBtn" type="submit">
+<span>Save &amp; SignIn</span>
+</button>
+ </li>
+ </ul>
+ </fieldset>
+ </form>
+ <input type="hidden" id="gmeUrl" value="https://www.googleapis.com/mapsengine/v1/tables/06739517320133004748-11853667273131550346/features?version=published&amp;key=AIzaSyCzwiHW1tSp_4FXaFuORRffbxBzQUN1qs4">
+ <input type="hidden" id="gmClientId" value="gme-macysinc">
+ <input type="hidden" id="gmeAPIKey" value="AIzaSyCzwiHW1tSp_4FXaFuORRffbxBzQUN1qs4">
+ <input type="hidden" id="gmeTableId" value="06739517320133004748-11853667273131550346">
+ <input type="hidden" id="gmeToSdpEnabled" value="true">
+ <input type="hidden" id="macysCookieDomain" value=".macys.com">
+ <input type="hidden" id="MACYS_secureHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_baseHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_assetsHostName" value="https://www.macys.com">
+ <input type="hidden" id="MACYS_imageHostName" value="/img/ts/is/image/MCY">
+ <input type="hidden" id="AKAMAI_LOGIC" value="hybrid">
+ <input type="hidden" id="searchBoxPlaceholderGuest" value="Search or enter web ID">
+ <input type="hidden" id="searchBoxPlaceholderUser" value="[userName], Search or enter web ID">
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en-us">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Newegg.com - Billing Info</title>
+ <meta name="keywords" content="Newegg.com - Once You Know, You Newegg">
+ <meta name="description" content="Newegg.com offers the best prices on computer parts, laptop computers, digital cameras, electronics and more with fast shipping and top-rated customer service. Once you know, you Newegg!">
+ <meta name="google-translate-customization" content="d08b8c829bab9a46-ac914e23c0a3607a-g7fba27d07436db8a-e">
+ <meta name="language" content="english">
+ <meta name="copyright" content="© 2000-2017 Newegg Inc.">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="robots" content="index,follow">
+ </head>
+ <body>
+ <form id="checkout" name="checkout" action="https://secure.newegg.com/GlobalShopping/CheckoutStep2.aspx?CartID=415%2b903ZRCECSZWBJE0&amp;IsCombineGuest=1" method="post" novalidate="novalidate">
+ <div>
+ Redeem Newegg gift cards<span>
+</span>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <label>Card Number</label>
+ <input type="text" name="GiftCode1"
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: UNKNOWN_TYPE
+ label: Card Number
+ parseable name: GiftCode1
+ field signature: 2868641577
+ form signature: 16660402300910943442"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ <input type="text" name="GiftCode" id="GiftCode" size="20" maxlength="16" autocomplete="off"
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: UNKNOWN_TYPE
+ label: Card Number
+ parseable name: GiftCode
+ field signature: 1229209062
+ form signature: 16660402300910943442"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ <div>
+ <label>Security Code</label>
+ <input type="password" name="ScurityCode1"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Security Code
+ parseable name: ScurityCode1
+ field signature: 828427756
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <input type="password" maxlength="4" name="SecurityCode" id="SecurityCode" autocomplete="off"
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: Security Code
+ parseable name: SecurityCode
+ field signature: 3977890449
+ form signature: 16660402300910943442"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ </div>
+ </div>
+ <div>
+ <a>
+ <i>
+</i>
+ Enter A Gift Card
+ <i>
+</i>
+ </a>
+ </div>
+ <input type="hidden" name="AllGiftCodes" id="AllGiftCodes" value="">
+ <input type="hidden" name="AllGiftPwds" id="AllGiftPwds" value="">
+ <input type="hidden" name="GiftMethodAction" id="GiftMethodAction" value="0">
+ </div>
+ <div>
+ <img
+title="EggPoints" alt="EggPoints" src="./Newegg.com - Billing Info_files/none.gif">
+ </div>
+ </div>
+ <ul>
+ <li>
+ <div id="GiftCardEmptyDiv">
+ <div id="GiftCardEmptyMsg">
+ <div>
+ <div>
+ <div>
+</div>
+ <div>
+<span>Missing Information </span>Card Number and Security Code fields cannot be empty. Please enter valid information and try again.</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ <div>
+ Payment Methods
+ <a rel="modal"
+title="What&#39;s this?" id="PaymentMethodDisabled">
+</a>
+ <div id="PaymentMethodDisabled_Content">
+ <p>Some payment methods may not be eligible for your order. Please review the full list by clicking <a>here</a> for payment restrictions.</p>
+ </div>
+ <span>
+</span>
+ </div>
+ <div>
+ <ul>
+ <li>
+ <input type="radio" name="paymentmethod" id="NSCC"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Newegg Store Credit Card
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="NSCC">
+ <strong>Newegg Store Credit Card</strong>
+ </label>
+ <div>
+ <div>
+ <div>
+ <img src="./Newegg.com - Billing Info_files/newegg-cc.png" alt="Newegg Store Credit Card">
+ <p>
+ </p>
+ </div>
+ <div>
+ <strong>Newegg Store Credit Card</strong>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="StoreCard_HolderName">
+ Cardholder Name
+ <span>(exactly as shown on card)</span>
+ </label>
+ <input
+title="overall type: CREDIT_CARD_NAME_FULL
+ server type: CREDIT_CARD_NAME_FULL
+ heuristic type: CREDIT_CARD_NAME_FULL
+ label: Cardholder Name (exactly as shown on card)
+ parseable name: StoreCard_HolderName
+ field signature: 853107187
+ form signature: 16660402300910943442" type="text" maxlength="80" name="StoreCard_HolderName" id="StoreCard_HolderName" value=""
+autofill-prediction="CREDIT_CARD_NAME_FULL"
+>
+ </div>
+ <div>
+ <label for="StoreCard_Number">
+ Card Number
+ </label>
+ <input
+title="overall type: CREDIT_CARD_NUMBER
+ server type: CREDIT_CARD_NUMBER
+ heuristic type: CREDIT_CARD_NUMBER
+ label: Card Number
+ parseable name: StoreCard_Number
+ field signature: 3054139089
+ form signature: 16660402300910943442" datavalid="60345916" autocomplete="off" id="StoreCard_Number" type="text" name="StoreCard_Number" value="" focus="false"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ </div>
+ <div>
+ <div>
+ <label>
+ <input type="checkbox" checked="checked" name="saveNsccCard" id="saveCard"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save for future orders
+ parseable name: saveNsccCard
+ field signature: 2599746278
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ Save for future orders
+ </label>
+ </div>
+ </div>
+ </div>
+ <div>
+</div>
+ </div>
+ <div>Do not have a Newegg Store Credit Card? <a>Learn More</a>
+</div>
+ </div>
+ </li>
+ <li>
+ <input type="radio" name="paymentmethod" value="Amex Express Checkout" id="amex"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Amex Express Checkout
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="amex">
+ <strong>Amex Express Checkout</strong>
+ </label>
+ <div>
+ <div>
+ <div>
+ <img src="./Newegg.com - Billing Info_files/AEC_PaymentMark_Blue_48x30.png" alt="Amex Express Checkout Button">
+ <p>
+ </p>
+ </div>
+ <div>
+ <strong>Checkout faster with American Express</strong>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li>
+ <input type="radio" name="paymentmethod" id="bitcoin" disabled="disabled"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Bitcoin
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="bitcoin">
+ <strong>Bitcoin</strong>
+ </label>
+ <div>
+ <div>
+ <div>
+ <img alt="Bitcoin accepted here"
+title="Bitcoin accepted here" src="./Newegg.com - Billing Info_files/bitcoin_logo.png">
+ <p>
+ </p>
+ </div>
+ <div>
+ <strong>Bitcoin is the safest and most secure way to pay online.</strong>
+ <ul>
+ <li>No identity theft risk; no payment information is ever stored.</li>
+ <li>Your payment completes immediately.</li>
+ <li>
+<a target="_blank">Learn more
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+</div>
+ <div>
+<span>NOTE: </span>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.<br>
+<br>
+ 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.
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <input type="hidden" name="IsMasterPassLightBoxEnable" id="IsMasterPassLightBoxEnable" value="True">
+ <li>
+ <input type="radio" name="paymentmethod" id="masterpass"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: MasterPass
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="masterpass">
+ <strong>MasterPass</strong>
+ </label>
+ <div>
+ <div>
+ <div>
+ <img src="./Newegg.com - Billing Info_files/mp_mc_acc_038px_gif.gif" alt="MasterPass">
+ <p>
+ </p>
+ </div>
+ <div>
+ <strong>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.
+ </strong>
+ <ul>
+ <li>
+ When you are ready to check out online, click on the "Buy with MasterPass" button.
+ </li>
+ <li>
+ Next, unlock your MasterPass account to choose your payment method and shipping address.
+ </li>
+ <li>
+ Then simply confirm your purchase, and you are done!
+ </li>
+ <li>
+ Use it with all major credit, debit, and prepaid cards.
+ </li>
+ <li>
+ Use it to easily store all your cards and addresses in one place.
+ </li>
+ <li>
+ Use it on all your connected devices.
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li>
+ <input type="radio" name="paymentmethod" id="visacheckout"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Visa Checkout
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="visacheckout">
+ <strong>Visa Checkout</strong>
+ </label>
+ <div>
+ <div>
+ <div>
+ <span id="#visacheckoutlearnmore">
+<img src="./Newegg.com - Billing Info_files/Cart_LearnMore_Pop-Up_left.png" alt="Visa Checkout">
+</span>
+ <img src="./Newegg.com - Billing Info_files/POS_horizontal_99x34.png" alt="Visa Checkout">
+ <ul>
+ <li>
+<a>
+ Learn more
+ </a>
+ </li>
+ </ul>
+ <p>
+ </p>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li>
+ <input type="radio" name="paymentmethod" id="paypal"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: PayPal
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="paypal">
+ <strong>PayPal</strong>
+ </label>
+ <div>
+ <div>
+ <div>
+ <img src="./Newegg.com - Billing Info_files/pp-acceptance-medium.png" alt="newegg">
+ <ul>
+ <li>
+ Speed through checkout.
+ </li>
+ <li>
+ PayPal is the safer, easier way to pay.
+ </li>
+ <li>
+<a>
+ What is PayPal?
+ </a>
+ </li>
+ </ul>
+ <p>
+ </p>
+ </div>
+ <div>
+ <strong>
+ PayPal is the safer, easier way to pay
+ </strong>
+ <ul>
+ <li>
+ Never expose your credit card number.
+ </li>
+ <li>
+ Speed through checkout all over the web. One account, one password - no need to retype your shipping or financial information.
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li>
+ <input type="radio" name="paymentmethod" id="creditcard"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Credit Card
+ parseable name: paymentmethod
+ field signature: 1614873398
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="creditcard">
+ <strong>Credit Card</strong>
+ </label>
+ <div id="CreditCardArea">
+ <dl id="CardList">
+ <div id="div_cardcvv2">
+ <div>
+ <label>
+ <span>Security Code (CVV2)</span>
+ <input
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: Security Code (CVV2)
+ parseable name: creditCardCVV2
+ field signature: 2862210423
+ form signature: 16660402300910943442" data-msg-validatecvv2="Security Code is invalid." type="text" value="" maxlength="4" id="creditCardCVV2" autocomplete="off"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ <a
+title="What is this?" rel="modal" id="CVV2CodeHelper">
+ <img src="./Newegg.com - Billing Info_files/cvv2_sm.gif" alt="cvv2">
+</a>
+ </label>
+ </div>
+ </div>
+ <dt>
+ <input id="AddNewCareditCard" name="cardList_R" type="radio" value="new"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Add New Credit Card
+ parseable name: cardList_R
+ field signature: 513212207
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <div>
+ <label for="AddNewCareditCard">Add New Credit Card</label>
+ </div>
+ </dt>
+ <dd>
+ <div>
+ <div>
+ <p>
+ <strong>All major credit cards accepted:</strong>
+ <span>
+ <img id="ImgDiscover" graysrc="https://ssl-images.newegg.com/WebResource/Themes/2005/Nest/img_ccDnetwork_v1_grey.gif" src="./Newegg.com - Billing Info_files/img_ccDnetwork_v1.gif" alt="Major Cards Accepted">
+ <img id="ImgMastercard" graysrc="https://ssl-images.newegg.com/WebResource/Themes/2005/Nest/img_ccMastercard_grey.gif" src="./Newegg.com - Billing Info_files/img_ccMastercard.gif" alt="Major Cards Accepted">
+ <img id="ImgAmex" graysrc="https://ssl-images.newegg.com/WebResource/Themes/2005/Nest/img_ccAmex_grey.gif" src="./Newegg.com - Billing Info_files/img_ccAmex.gif" alt="Major Cards Accepted">
+ <img id="ImgVisa" graysrc="https://ssl-images.newegg.com/WebResource/Themes/2005/Nest/img_ccVisa_grey.gif" src="./Newegg.com - Billing Info_files/img_ccVisa.gif" alt="Major Cards Accepted">
+ </span>
+ </p>
+ </div>
+ <div>
+ <label for="Card_HolderNameNew">
+ Cardholder Name
+ <span>(exactly as shown on card)</span>
+ </label>
+ <input
+title="overall type: CREDIT_CARD_NAME_FULL
+ server type: CREDIT_CARD_NAME_FULL
+ heuristic type: CREDIT_CARD_NAME_FULL
+ label: Cardholder Name (exactly as shown on card)
+ parseable name: Card_HolderNameNew
+ field signature: 3439434497
+ form signature: 16660402300910943442" type="text" maxlength="80" name="Card_HolderNameNew" id="Card_HolderNameNew" value="" focus="false"
+autofill-prediction="CREDIT_CARD_NAME_FULL"
+>
+ </div>
+ <div>
+ <label for="Card_CCNUMBERNEW">
+ Card Number
+ </label>
+ <input
+title="overall type: CREDIT_CARD_NUMBER
+ server type: CREDIT_CARD_NUMBER
+ heuristic type: CREDIT_CARD_NUMBER
+ label: Card Number
+ parseable name: Card_CCNUMBERNEW
+ field signature: 1223289945
+ form signature: 16660402300910943442" autocomplete="off" id="Card_CCNUMBERNEW" type="text" name="Card_CCNUMBERNEW" value="" onkeyup="if (&#39;True&#39; == &#39;True&#39;) {
+ Biz.Payment.AmexPoints.OnChangeOfCardNumberForNewInGlobalCheckoutStep2();}" focus="false"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ <img alt="Major Cards Accepted">
+ </div>
+ <div>
+ <label for="Card_exp_monthNew">
+ Expiration Date
+ </label>
+ <select name="Card_exp_monthNew" id="Card_exp_monthNew" size="1" focus="false"
+title="overall type: CREDIT_CARD_EXP_MONTH
+ server type: CREDIT_CARD_EXP_MONTH
+ heuristic type: CREDIT_CARD_EXP_MONTH
+ label: Expiration Date
+ parseable name: Card_exp_monthNew
+ field signature: 52703496
+ form signature: 16660402300910943442"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option value="Month">Month</option>
+ <option value="01">01</option>
+ <option value="02">02</option>
+ <option value="03">03</option>
+ <option value="04">04</option>
+ <option value="05">05</option>
+ <option value="06">06</option>
+ <option value="07">07</option>
+ <option value="08">08</option>
+ <option value="09">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ <select name="Card_exp_yearNew" id="Card_exp_yearNew" size="1" focus="false"
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ server type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ heuristic type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ label: Expiration Date
+ parseable name: Card_exp_yearNew
+ field signature: 3677977052
+ form signature: 16660402300910943442"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option value="Year">Year</option>
+ <option value="2017">2017</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ <option value="2028">2028</option>
+ <option value="2029">2029</option>
+ <option value="2030">2030</option>
+ </select>
+ </div>
+ <div>
+ <label for="cvv2code">
+ <span>Security Code (CVV2)</span>
+ </label>
+ <input
+title="overall type: CREDIT_CARD_VERIFICATION_CODE
+ server type: NO_SERVER_DATA
+ heuristic type: CREDIT_CARD_VERIFICATION_CODE
+ label: Security Code (CVV2)
+ parseable name: cvv2code
+ field signature: 1678191663
+ form signature: 16660402300910943442" data-msg-validatecvv2="Security Code is invalid." type="text" value="" maxlength="4" name="cvv2code" id="cvv2code" autocomplete="off"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+>
+ <a
+title="What is this?" rel="modal" id="CVV2CodeHelper">
+ <img src="./Newegg.com - Billing Info_files/cvv2_sm.gif" alt="cvv2">
+</a>
+ </div>
+ <div>
+ <div>
+ <label>
+ <input type="checkbox" checked="checked" name="saveCard" id="saveCard"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save for future orders
+ parseable name: saveCard
+ field signature: 4084541912
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ Save for future orders
+ </label>
+ </div>
+ <div>
+ <label>
+ Save Credit Card As:
+ <input type="text" value="untitled" maxlength="80" id="Card_PaytermLabel" name="Card_PaytermLabel"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save Credit Card As:
+ parseable name: Card_PaytermLabel
+ field signature: 3910487729
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </label>
+ </div>
+ <div>
+ <label>
+ <input type="checkbox" checked="checked" name="IsNotDefault" id="makeDefaultOptionNew"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Set as default credit card
+ parseable name: IsNotDefault
+ field signature: 3972010007
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ Set as default credit card
+ </label>
+ </div>
+ </div>
+ </div>
+ <div>
+</div>
+ <input type="hidden" name="Card_BankPhone" id="Card_BankPhone" value="888-888-8888">
+ <input type="hidden" name="Card_TransactionNumber" id="Card_TransactionNumber" value="">
+ <input type="hidden" name="Card_HolderName" id="Card_HolderName" value="">
+ <input type="hidden" name="Card_Number" id="Card_Number" value="">
+ <input type="hidden" name="ReEnter_Card_Number" id="ReEnter_Card_Number" value="">
+ <input type="hidden" name="IS_Mark_Default" id="IS_Mark_Default" value="">
+ <input type="hidden" name="Card_exp_month" id="Card_exp_month" value="">
+ <input type="hidden" name="Card_exp_year" id="Card_exp_year" value="">
+ <input type="hidden" name="Card_CVV2" id="Card_CVV2" value="">
+ <input type="hidden" name="Card_IsDefault" id="Card_IsDefault" value="">
+ <input type="hidden" name="Card_CCTYPE" id="Card_CCTYPE" value="">
+ <input type="hidden" name="IsUsedAmexSaveCard" id="IsUsedAmexSaveCard" value="false">
+ <input type="hidden" id="defalutSelectCreditCardRow" value="AddNewCareditCard">
+ <input type="hidden" name="supportCTypes" id="supportCTypes" value="4,5,6,3">
+ <input type="hidden" name="IsAMEXDistributedCard" value="False">
+ <input type="hidden" name="AMEXDistributedCard" value="">
+ <input type="hidden" name="AMEXDistributedCardVI" value="">
+ <input type="hidden" name="AMEXDistributedCardRequestID" value="">
+ <input type="hidden" id="cardNumber-1" value="">
+ </dd>
+ </dl>
+ </div>
+ </li>
+ </ul>
+ <input type="hidden" id="defaultSelectPaytermMethod" value="creditcard" readonly="readonly">
+ </div>
+ <div>
+ Billing Address
+ <span>
+</span>
+ </div>
+ <ul>
+ <li id="address-chooser">
+ <input type="checkbox" id="billingsameaddress" name="IsBilling" checked="" value="yes"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Same as the shipping address
+ parseable name: IsBilling
+ field signature: 1298880426
+ form signature: 16660402300910943442"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <label for="billingsameaddress">
+ Same as the shipping address
+ </label>
+ </li>
+ <li id="same-address">
+ </li>
+ <li id="new-address">
+ <div>
+ <input type="hidden" name="STransNumber" value="0">
+ <input type="hidden" name="action" value="">
+ <input type="hidden" name="OldContactWith" value="">
+ <ul>
+ <li>
+ <label>
+ Country
+ <span>(<strong>NOTE: </strong>
+<em>Billing country cannot be different from shipping country you selected in previous step.</em>)</span>
+ </label>
+ <select name="SCountry_Option" selectcountry="USA" id="countryOption" tabindex="0" size="1"
+title="overall type: ADDRESS_HOME_COUNTRY
+ server type: ADDRESS_HOME_COUNTRY
+ heuristic type: ADDRESS_HOME_COUNTRY
+ label: Country (NOTE: Billing country cannot be different from shipping country you selected in previous st
+ parseable name: SCountry_Option
+ field signature: 3459145984
+ form signature: 16660402300910943442"
+autofill-prediction="ADDRESS_HOME_COUNTRY"
+>
+ <option value="USA">United States</option>
+ </select>
+ </li>
+ <li>
+ <label>Address</label>
+ <input
+title="overall type: ADDRESS_HOME_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address
+ parseable name: SAddress1
+ field signature: 328501296
+ form signature: 16660402300910943442" type="text" name="SAddress1" id="SAddress1" maxlength="100" value="" placeholder=""
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </li>
+ <li>
+ <label>
+ Address 2
+ <span>
+<strong>(OPTIONAL)</strong>
+<em>Apartment, suite, floor, etc.</em>
+</span>
+</label>
+ <input
+title="overall type: ADDRESS_HOME_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address 2 (OPTIONAL) Apartment, suite, floor, etc.
+ parseable name: SAddress2
+ field signature: 26232370
+ form signature: 16660402300910943442" type="text" name="SAddress2" id="SAddress2" maxlength="100" value="" placeholder=""
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </li>
+ <li>
+ <ul>
+ <li>
+ <div>
+ <label id="lblCity">City</label>
+ <input
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: City
+ parseable name: SCity
+ field signature: 1173963820
+ form signature: 16660402300910943442" type="text" name="SCity" id="SCity" maxlength="45" value=""
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ </li>
+ <li>
+ <div>
+ <label for="stateDropDownList" id="lblState">State/Province/Region</label>
+ <div>
+ <select selectstate="CA" name="SState_Option" id="SState_Option_USA" tabindex="0" size="1"
+title="overall type: ADDRESS_HOME_STATE
+ server type: ADDRESS_HOME_STATE
+ heuristic type: ADDRESS_HOME_STATE
+ label: State/Province/Region
+ parseable name: SState_Option
+ field signature: 3077158397
+ form signature: 16660402300910943442"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="AA">AA</option>
+ <option value="AP">AP</option>
+ <option value="AE">AE</option>
+ <option value="AL">ALABAMA</option>
+ <option value="AK">ALASKA</option>
+ <option value="AS">AMERICAN SAMOA</option>
+ <option value="AZ">ARIZONA</option>
+ <option value="AR">ARKANSAS</option>
+ <option value="CA">CALIFORNIA</option>
+ <option value="CO">COLORADO</option>
+ <option value="CT">CONNECTICUT</option>
+ <option value="DE">DELAWARE</option>
+ <option value="DC">DISTRICT OF COLUMBIA</option>
+ <option value="FL">FLORIDA</option>
+ <option value="GA">GEORGIA</option>
+ <option value="HI">HAWAII</option>
+ <option value="ID">IDAHO</option>
+ <option value="IL">ILLINOIS</option>
+ <option value="IN">INDIANA</option>
+ <option value="IA">IOWA</option>
+ <option value="KS">KANSAS</option>
+ <option value="KY">KENTUCKY</option>
+ <option value="LA">LOUISIANA</option>
+ <option value="ME">MAINE</option>
+ <option value="MD">MARYLAND</option>
+ <option value="MA">MASSACHUSETTS</option>
+ <option value="MI">MICHIGAN</option>
+ <option value="MN">MINNESOTA</option>
+ <option value="MS">MISSISSIPPI</option>
+ <option value="MO">MISSOURI</option>
+ <option value="MT">MONTANA</option>
+ <option value="NE">NEBRASKA</option>
+ <option value="NV">NEVADA</option>
+ <option value="NH">NEW HAMPSHIRE</option>
+ <option value="NJ">NEW JERSEY</option>
+ <option value="NM">NEW MEXICO</option>
+ <option value="NY">NEW YORK</option>
+ <option value="NC">NORTH CAROLINA</option>
+ <option value="ND">NORTH DAKOTA</option>
+ <option value="OH">OHIO</option>
+ <option value="OK">OKLAHOMA</option>
+ <option value="OR">OREGON</option>
+ <option value="PA">PENNSYLVANIA</option>
+ <option value="PR">PUERTO RICO</option>
+ <option value="RI">RHODE ISLAND</option>
+ <option value="SC">SOUTH CAROLINA</option>
+ <option value="SD">SOUTH DAKOTA</option>
+ <option value="TN">TENNESSEE</option>
+ <option value="TX">TEXAS</option>
+ <option value="VI">U.S. Virgin Islands</option>
+ <option value="UT">UTAH</option>
+ <option value="VT">VERMONT</option>
+ <option value="VA">VIRGINIA</option>
+ <option value="WA">WASHINGTON</option>
+ <option value="WV">WEST VIRGINIA</option>
+ <option value="WI">WISCONSIN</option>
+ <option value="WY">WYOMING</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label id="lblZip">Zip/Postal Code</label>
+ <input
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: Zip/Postal Code
+ parseable name: SZip
+ field signature: 3887080504
+ form signature: 16660402300910943442" maxlength="20" size="20" type="text" name="SZip" id="SZip" value=""
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </li>
+ <li>
+ <label>
+ Phone
+ </label>
+ <input
+title="overall type: PHONE_HOME_CITY_AND_NUMBER
+ server type: PHONE_HOME_CITY_AND_NUMBER
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: Phone
+ parseable name: ShippingPhone
+ field signature: 3574122515
+ form signature: 16660402300910943442" type="text" name="ShippingPhone" maxlength="30" id="ShippingPhone" value=""
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </li>
+ <li>
+ <label>
+ &nbsp;
+ </label>
+ <label>
+ </label>
+ </li>
+ <input type="hidden" id="IsDefault" name="IsDefault">
+ <input type="hidden" name="SContactWith" value="Tester Mo">
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </li>
+ <input type="hidden" name="BCountry" value="USA">
+ <input type="hidden" name="BAddress1" value="331 E. Evelyn Avenue">
+ <input type="hidden" name="BAddress2" value="">
+ <input type="hidden" name="BCity" value="Mountain View">
+ <input type="hidden" name="BState" value="CA">
+ <input type="hidden" name="BZip" value="94041">
+ <input type="hidden" name="CPCPostalCodeForDisplay" value="">
+ <input type="hidden" name="IsCanadaPost" value="False">
+ <input type="hidden" name="BPhone" value="650-903-0800">
+ <input type="hidden" name="BContactWith" value="Tester Mo">
+ <input type="hidden" name="hiddenSCountry" value="USA">
+ <input type="hidden" name="BIsInPost" value="False">
+ </ul>
+ <input type="hidden" id="cfAppendix" name="cfAppendix">
+ <input type="hidden" id="isIgnoreBillingFocus" value="False">
+ <input type="hidden" name="IsReEnterCreditCardValid" id="IsReEnterCreditCardValid" value="True">
+ <input type="hidden" name="VMESuccessfulReturnInfo" id="VMESuccessfulReturnInfo">
+ <input type="hidden" name="SubmitTypeValue" id="SubmitTypeValue" value="">
+ <input type="hidden" name="lastPurchaseDate" id="lastPurchaseDate" value="1/1/0001 12:00:00 AM">
+ <input type="hidden" id="GoogleWalletMaskedRequest" name="GoogleWalletMaskedRequest" value="">
+ <input type="hidden" id="GoogleWalletMaskedResponse" name="GoogleWalletMaskedResponse">
+ <input type="hidden" name="IsEnoughGCAmount" value="0">
+ <input type="hidden" name="CustomerCardCVV2IsRequired" id="CustomerCardCVV2IsRequired" value="1">
+ <input type="hidden" name="CustomerAppendix" id="CustomerAppendix" value="+KrrFNdT/kbgvDIajN498xXVkpXHtGV4IGEz2zWZhtCaE5W/pB02+iKMnppgs68Q">
+ </form>
+ <form action="https://secure.newegg.com/Common/Ajax/SavePaymentByAjax.aspx" method="POST" id="creditCardCommandEdit_Form" name="creditCardCommandEdit_Form" novalidate="novalidate">
+ <div>
+ <div>
+</div>
+ <ul>
+ <li>
+ <label for="Card_PaytermLabel">
+ Save As
+ <span>
+ <em>(a nickname for easy recognition)</em>
+ </span>
+ </label>
+ <input type="text" value="" id="Card_PaytermLabel" name="Card_PaytermLabel" maxlength="80"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Save As (a nickname for easy recognition)
+ parseable name: Card_PaytermLabel
+ field signature: 3910487729
+ form signature: 15313939527407611188"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </li>
+ <li>
+ <label for="Card_HolderName">
+ Cardholder Name
+ <span>
+ <em>(exactly as shown on card)</em>
+ </span>
+ </label>
+ <input
+title="overall type: CREDIT_CARD_NAME_FULL
+ server type: CREDIT_CARD_NAME_FULL
+ heuristic type: CREDIT_CARD_NAME_FULL
+ label: Cardholder Name (exactly as shown on card)
+ parseable name: Card_HolderName
+ field signature: 2535895036
+ form signature: 15313939527407611188" type="text" maxlength="80" name="Card_HolderName" id="Card_HolderName" focus="false"
+autofill-prediction="CREDIT_CARD_NAME_FULL"
+>
+ </li>
+ <li>
+ <label for="Card_Number">
+ Card Number
+ </label>
+ <input
+title="overall type: CREDIT_CARD_NUMBER
+ server type: CREDIT_CARD_NUMBER
+ heuristic type: CREDIT_CARD_NUMBER
+ label: Card Number
+ parseable name: Card_Number
+ field signature: 3930555619
+ form signature: 15313939527407611188" autocomplete="off" id="Card_Number" type="text" name="Card_Number" focus="false"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ <img id="CardImg" name="CardImg" alt="Major Cards Accepted">
+ </li>
+ <li>
+ <label for="Card_exp_month">
+ Expiration Date
+ </label>
+ <select name="Card_exp_month" id="Card_exp_month" size="1" focus="false"
+title="overall type: CREDIT_CARD_EXP_MONTH
+ server type: CREDIT_CARD_EXP_MONTH
+ heuristic type: CREDIT_CARD_EXP_MONTH
+ label: Expiration Date
+ parseable name: Card_exp_month
+ field signature: 3840696256
+ form signature: 15313939527407611188"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option value="Month">Month</option>
+ <option value="01">01</option>
+ <option value="02">02</option>
+ <option value="03">03</option>
+ <option value="04">04</option>
+ <option value="05">05</option>
+ <option value="06">06</option>
+ <option value="07">07</option>
+ <option value="08">08</option>
+ <option value="09">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ <select name="Card_exp_year" id="Card_exp_year" size="1" focus="false"
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ server type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ heuristic type: CREDIT_CARD_EXP_4_DIGIT_YEAR
+ label: Expiration Date
+ parseable name: Card_exp_year
+ field signature: 185580832
+ form signature: 15313939527407611188"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option value="Year">Year</option>
+ <option value="2017">2017</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ <option value="2028">2028</option>
+ <option value="2029">2029</option>
+ <option value="2030">2030</option>
+ </select>
+ </li>
+ <li>
+ <label id="makeCCDefaultOptionNew">
+ <input type="checkbox" id="makeDefaultOptionNew" name="Card_IsDefault"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Set as default credit card
+ parseable name: Card_IsDefault
+ field signature: 1494055862
+ form signature: 15313939527407611188"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ Set as default credit card
+ </label>
+ </li>
+ <li>
+</li>
+ </ul>
+ </div>
+ <input type="hidden" name="IsInBillingPage" id="IsInBillingPage" value="True">
+ <input type="hidden" name="action" value="edit">
+ <input type="hidden" name="selectIndex" value="">
+ <input type="hidden" name="Card_CCTYPE" id="Card_CCTYPE" value="">
+ <input type="hidden" name="Card_BankPhone" id="Card_BankPhone" value="888-888-8888">
+ <input type="hidden" name="Card_TransactionNumber" id="Card_TransactionNumber" value="">
+ <input type="hidden" name="supportCTypes" id="supportCTypes" value="4,5,6,3">
+ </form>
+ <form action="https://secure.newegg.com/Common/Ajax/SavePaymentByAjax.aspx" method="POST" id="creditCardCommandRemove_Form" name="creditCardCommandRemove_Form" novalidate="novalidate">
+ <div>
+ <div>
+</div>
+ </div>
+ <input type="hidden" name="action" value="remove">
+ <input type="hidden" name="selectIndex" value="">
+ <input type="hidden" name="Card_TransactionNumber" value="">
+ </form>
+ <form action="https://secure.newegg.com/Common/Ajax/SavePaymentByAjax.aspx" method="POST" id="NsccCardCommandEdit_Form" name="NsccCardCommandEdit_Form" novalidate="novalidate">
+ <div>
+ <div>
+</div>
+ <ul>
+ <li>
+ <label for="Card_HolderName">
+ Cardholder Name
+ <span>
+ <em>(exactly as shown on card)</em>
+ </span>
+ </label>
+ <input
+title="Cardholder Name" type="text" maxlength="80" name="StoreCard_HolderName" id="StoreCard_HolderName" focus="false">
+ </li>
+ <li>
+ <label for="StoreCard_Number">
+ Card Number
+ </label>
+ <input
+title="Card Number" datavalid="60345916" autocomplete="off" id="StoreCard_Number" type="text" name="StoreCard_Number" focus="false">
+ </li>
+ <li>
+ </li>
+ <input type="hidden" name="action" value="nsccedit">
+ <input type="hidden" name="StoreCard_TransactionNumber" value="">
+ </ul>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en-us">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Newegg.com - Login</title>
+ <meta name="keywords" content="Newegg.com - Once You Know, You Newegg">
+ <meta name="description" content="Newegg.com offers the best prices on computer parts, laptop computers, digital cameras, electronics and more with fast shipping and top-rated customer service. Once you know, you Newegg!">
+ <meta name="google-translate-customization" content="d08b8c829bab9a46-ac914e23c0a3607a-g7fba27d07436db8a-e">
+ <meta name="language" content="english">
+ <meta name="copyright" content="© 2000-2017 Newegg Inc.">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="robots" content="index,follow">
+ </head>
+ <body>
+ <form method="post" action="https://secure.newegg.com/Shopping/ShoppingLogin.aspx" name="loginForm" id="loginForm" novalidate="novalidate">
+ <span id="CaptchaS">
+</span>
+ <input type="hidden" name="nextpage" value="https://secure.newegg.com/GlobalShopping/CheckoutStep1.aspx?CartID=797%2b903ZRCECSZWBJE0&amp;PaymentType=SIMPLECHECKOUT&amp;IsFromCart=1">
+ <input type="hidden" name="StuTargetPage" value="">
+ <input type="hidden" name="action" value="login">
+ <input type="hidden" name="ShoppingLoginFlag" value="0">
+ <input type="hidden" name="SPSign" value="">
+ <input type="hidden" name="LoginAppendix" value="x0kFyaA7FPQ7xpTt6JRs7tjF3jD2dY/TYhVkLYYFvo6cyOJwvqVUNlxxUjSZYUB0">
+ <ul>
+ <li>
+ <label for="UserName">
+ Newegg ID
+ <span>
+ (Typically your email address)
+ </span>
+ </label>
+ <input type="text" tabindex="5" maxlength="128" size="15"
+title="Email Address" id="UserName" name="UserName" value="">
+ </li>
+ <li>
+ <label for="UserPwd">
+ Password
+ </label>
+ <input type="password" tabindex="6" maxlength="30" size="17"
+title="Password" id="UserPwd" name="UserPwd">
+ </li>
+ <li>
+ <input type="checkbox" value="1" id="IsRemember" name="IsRemember" tabindex="8">
+<label for="IsRemember">Remember Me</label>
+ </li>
+ <li id="guestloginforgot">
+<a
+title="Forgot your Newegg ID or Password?">
+ Forgot your Newegg ID or Password?
+ </a>
+ </li>
+ <li>
+ <div>
+ <div>
+ </div>
+ <a tabindex="7" id="submit">
+ SIGN IN <span>►</span>
+ </a>
+ </div>
+ </li>
+ <li>
+ <input type="checkbox" value="1" id="IsSubscribe" name="Newsletter" tabindex="9">
+<label for="IsSubscribe">Subscribe for exclusive e-mail offers and discounts</label>
+ </li>
+ </ul>
+ <input type="hidden" id="cfAppendix" name="cfAppendix">
+ <input type="hidden" name="LoginAdditional" value="V6QdNc8GTJxKoxj5ef1cRLAlFLzcWhGcVj7od0iDpBUAd2FabNw7ApfN0g6LtbiB7EHyhgAaqpOdFbyt3NLuwPc9y1fkPs72+5qzSWHzJu19uWH+4mAefmVtLbXxTeIyGjEzvpanJrgib/dw61t1qZBot9DAigxz">
+ <input type="hidden" name="IsPrevCaptcha" value="False">
+ <input type="hidden" id="GoogleReCAPTCHA" name="GoogleReCAPTCHA" value="False">
+ <input type="hidden" id="hiddenServerDateTimeNow" value="Mon, 20 Mar 2017 03:43:47 GMT">
+ <input type="hidden" id="hiddenAllowClientTimeDiffInMinute" value="10">
+ </form>
+ <form method="post" action="https://secure.newegg.com/Shopping/ShoppingLogin.aspx" name="guestCheckOutForm" id="guestCheckOutForm" novalidate="novalidate">
+ <input type="hidden" name="action" value="guestckeckout">
+ <a rel="">CONTINUE AS A GUEST <span>►</span>
+</a>
+ </form>
+ <form name="registerForm" id="registerForm" method="post" action="https://secure.newegg.com/Shopping/ShoppingLogin.aspx" novalidate="novalidate">
+ <input type="hidden" value="https://secure.newegg.com/GlobalShopping/CheckoutStep1.aspx?CartID=797%2b903ZRCECSZWBJE0&amp;PaymentType=SIMPLECHECKOUT&amp;IsFromCart=1" name="NextPage">
+ <input type="hidden" name="ShoppingLoginFlag" value="0">
+ <input type="hidden" name="action" value="register">
+ <input type="hidden" name="SPSign" value="">
+ <input type="hidden" name="LoginAppendix" id="LoginAppendix" value="x0kFyaA7FPQ7xpTt6JRs7tjF3jD2dY/TYhVkLYYFvo6cyOJwvqVUNlxxUjSZYUB0">
+ <input type="hidden" name="IsPrevCaptcha" value="False">
+ <ul>
+ <li>
+<label for="LoginName">Email Address</label>
+<input name="LoginName" id="LoginName" tabindex="10" maxlength="128"
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: Email Address
+ parseable name: LoginName
+ field signature: 1679789274
+ form signature: 17613573414325428514" value="" type="text"
+autofill-prediction="EMAIL_ADDRESS"
+>
+</li>
+ <li>
+<label for="Password">Password</label>
+<input name="Password" id="Password" tabindex="12" type="password"
+title="overall type: ACCOUNT_CREATION_PASSWORD
+ server type: ACCOUNT_CREATION_PASSWORD
+ heuristic type: UNKNOWN_TYPE
+ label: Password
+ parseable name: Password
+ field signature: 3935904101
+ form signature: 17613573414325428514"
+autofill-prediction="ACCOUNT_CREATION_PASSWORD"
+>
+</li>
+ <li>
+<label for="LoginName1">Confirm Email Address</label>
+<input name="LoginName1" tabindex="11" maxlength="128" id="LoginName1"
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: Confirm Email Address
+ parseable name: LoginName1
+ field signature: 2268781658
+ form signature: 17613573414325428514" value="" type="text"
+autofill-prediction="EMAIL_ADDRESS"
+>
+</li>
+ <li>
+<label for="Password1">Confirm Password</label>
+<input name="Password1" id="Password1" tabindex="13"
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Confirm Password
+ parseable name: Password1
+ field signature: 1108250005
+ form signature: 17613573414325428514" type="password"
+autofill-prediction="UNKNOWN_TYPE"
+>
+</li>
+ <li>
+</li>
+ <li>
+ <input id="IsSubscribe2" type="checkbox" value="1" name="Newsletter" tabindex="15" checked=""
+title="overall type: UNKNOWN_TYPE
+ server type: NO_SERVER_DATA
+ heuristic type: UNKNOWN_TYPE
+ label: Subscribe for exclusive e-mail offers and discounts
+ parseable name: Newsletter
+ field signature: 2681942707
+ form signature: 17613573414325428514"
+autofill-prediction="UNKNOWN_TYPE">
+<label for="IsSubscribe2"
+>Subscribe for exclusive e-mail offers and discounts</label>
+ </li>
+ </ul>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en-us">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Newegg.com - Shipping Info</title>
+ <meta name="keywords" content="Newegg.com - Once You Know, You Newegg">
+ <meta name="description" content="Newegg.com offers the best prices on computer parts, laptop computers, digital cameras, electronics and more with fast shipping and top-rated customer service. Once you know, you Newegg!">
+ <meta name="google-translate-customization" content="d08b8c829bab9a46-ac914e23c0a3607a-g7fba27d07436db8a-e">
+ <meta name="language" content="english">
+ <meta name="copyright" content="© 2000-2017 Newegg Inc.">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="robots" content="index,follow">
+ </head>
+ <body>
+ <form id="checkout-shipping" action="https://secure.newegg.com/GlobalShopping/CheckoutStep1.aspx?CartID=415%2b903ZRCECSZWBJE0&amp;IsCombineGuest=1" method="POST" novalidate="novalidate">
+ <input type="hidden" name="SubmitFlag" value="true">
+ <input type="hidden" name="STransNumber" value="0">
+ <input type="hidden" name="action" value="">
+ <input type="hidden" name="OldContactWith" value="">
+ <ul>
+ <li>
+ <label>
+ First Name
+ </label>
+ <input validgroup="g1"
+title="overall type: NAME_FIRST
+ server type: NAME_FIRST
+ heuristic type: NAME_FIRST
+ label: First Name
+ parseable name: SFirstName
+ field signature: 3883597444
+ form signature: 7987654844929240962" type="text" name="SFirstName" id="SFirstName" maxlength="80" value=""
+autofill-prediction="NAME_FIRST" enablevalid="true"
+>
+ </li>
+ <li>
+ <label>
+ Last Name
+ </label>
+ <input validgroup="g1"
+title="overall type: NAME_LAST
+ server type: NAME_LAST
+ heuristic type: NAME_LAST
+ label: Last Name
+ parseable name: SLastName
+ field signature: 1256168210
+ form signature: 7987654844929240962" type="text" name="SLastName" id="SLastName" maxlength="80" value=""
+autofill-prediction="NAME_LAST" enablevalid="true"
+>
+ </li>
+ <li>
+ <label>
+ Country
+ </label>
+ <select name="SCountry_Option" selectcountry="USA" id="countryOption" tabindex="0" size="1"
+title="overall type: ADDRESS_HOME_COUNTRY
+ server type: ADDRESS_HOME_COUNTRY
+ heuristic type: ADDRESS_HOME_COUNTRY
+ label: Country
+ parseable name: SCountry_Option
+ field signature: 3459145984
+ form signature: 7987654844929240962"
+autofill-prediction="ADDRESS_HOME_COUNTRY"
+>
+ <option value="USA">United States</option>
+ </select>
+ </li>
+ <li>
+ <label>Address</label>
+ <input
+title="overall type: ADDRESS_HOME_LINE1
+ server type: ADDRESS_HOME_LINE1
+ heuristic type: ADDRESS_HOME_LINE1
+ label: Address
+ parseable name: SAddress1
+ field signature: 328501296
+ form signature: 7987654844929240962" type="text" name="SAddress1" id="SAddress1" maxlength="100" value="" placeholder=""
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </li>
+ <li>
+ <label>
+ Address 2
+ <span>
+<strong>(OPTIONAL)</strong>
+<em>Apartment, suite, floor, etc.</em>
+</span>
+</label>
+ <input
+title="overall type: ADDRESS_HOME_LINE2
+ server type: ADDRESS_HOME_LINE2
+ heuristic type: ADDRESS_HOME_LINE2
+ label: Address 2 (OPTIONAL) Apartment, suite, floor, etc.
+ parseable name: SAddress2
+ field signature: 26232370
+ form signature: 7987654844929240962" type="text" name="SAddress2" id="SAddress2" maxlength="100" value="" placeholder=""
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </li>
+ <li>
+ <ul>
+ <li>
+ <div>
+ <label id="lblCity">City</label>
+ <input
+title="overall type: ADDRESS_HOME_CITY
+ server type: ADDRESS_HOME_CITY
+ heuristic type: ADDRESS_HOME_CITY
+ label: City
+ parseable name: SCity
+ field signature: 1173963820
+ form signature: 7987654844929240962" type="text" name="SCity" id="SCity" maxlength="45" value=""
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ </li>
+ <li>
+ <div>
+ <label for="stateDropDownList" id="lblState">State/Province/Region</label>
+ <div>
+ <select selectstate="" name="SState_Option" id="SState_Option_USA" tabindex="0" size="1"
+title="overall type: ADDRESS_HOME_STATE
+ server type: ADDRESS_HOME_STATE
+ heuristic type: ADDRESS_HOME_STATE
+ label: State/Province/Region
+ parseable name: SState_Option
+ field signature: 3077158397
+ form signature: 7987654844929240962"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="AA">AA</option>
+ <option value="AP">AP</option>
+ <option value="AE">AE</option>
+ <option value="AL">ALABAMA</option>
+ <option value="AK">ALASKA</option>
+ <option value="AS">AMERICAN SAMOA</option>
+ <option value="AZ">ARIZONA</option>
+ <option value="AR">ARKANSAS</option>
+ <option value="CA">CALIFORNIA</option>
+ <option value="CO">COLORADO</option>
+ <option value="CT">CONNECTICUT</option>
+ <option value="DE">DELAWARE</option>
+ <option value="DC">DISTRICT OF COLUMBIA</option>
+ <option value="FL">FLORIDA</option>
+ <option value="GA">GEORGIA</option>
+ <option value="HI">HAWAII</option>
+ <option value="ID">IDAHO</option>
+ <option value="IL">ILLINOIS</option>
+ <option value="IN">INDIANA</option>
+ <option value="IA">IOWA</option>
+ <option value="KS">KANSAS</option>
+ <option value="KY">KENTUCKY</option>
+ <option value="LA">LOUISIANA</option>
+ <option value="ME">MAINE</option>
+ <option value="MD">MARYLAND</option>
+ <option value="MA">MASSACHUSETTS</option>
+ <option value="MI">MICHIGAN</option>
+ <option value="MN">MINNESOTA</option>
+ <option value="MS">MISSISSIPPI</option>
+ <option value="MO">MISSOURI</option>
+ <option value="MT">MONTANA</option>
+ <option value="NE">NEBRASKA</option>
+ <option value="NV">NEVADA</option>
+ <option value="NH">NEW HAMPSHIRE</option>
+ <option value="NJ">NEW JERSEY</option>
+ <option value="NM">NEW MEXICO</option>
+ <option value="NY">NEW YORK</option>
+ <option value="NC">NORTH CAROLINA</option>
+ <option value="ND">NORTH DAKOTA</option>
+ <option value="OH">OHIO</option>
+ <option value="OK">OKLAHOMA</option>
+ <option value="OR">OREGON</option>
+ <option value="PA">PENNSYLVANIA</option>
+ <option value="PR">PUERTO RICO</option>
+ <option value="RI">RHODE ISLAND</option>
+ <option value="SC">SOUTH CAROLINA</option>
+ <option value="SD">SOUTH DAKOTA</option>
+ <option value="TN">TENNESSEE</option>
+ <option value="TX">TEXAS</option>
+ <option value="VI">U.S. Virgin Islands</option>
+ <option value="UT">UTAH</option>
+ <option value="VT">VERMONT</option>
+ <option value="VA">VIRGINIA</option>
+ <option value="WA">WASHINGTON</option>
+ <option value="WV">WEST VIRGINIA</option>
+ <option value="WI">WISCONSIN</option>
+ <option value="WY">WYOMING</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label id="lblZip">Zip/Postal Code</label>
+ <input
+title="overall type: ADDRESS_HOME_ZIP
+ server type: ADDRESS_HOME_ZIP
+ heuristic type: ADDRESS_HOME_ZIP
+ label: Zip/Postal Code
+ parseable name: SZip
+ field signature: 3887080504
+ form signature: 7987654844929240962" maxlength="20" size="20" type="text" name="SZip" id="SZip" value=""
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </li>
+ <li>
+ <label>
+ Phone
+ </label>
+ <input
+title="overall type: PHONE_HOME_CITY_AND_NUMBER
+ server type: PHONE_HOME_CITY_AND_NUMBER
+ heuristic type: PHONE_HOME_WHOLE_NUMBER
+ label: Phone
+ parseable name: ShippingPhone
+ field signature: 3574122515
+ form signature: 7987654844929240962" type="text" name="ShippingPhone" maxlength="30" id="ShippingPhone" value=""
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ </li>
+ <li>
+ <label>
+ Email
+ </label>
+ <input
+title="overall type: EMAIL_ADDRESS
+ server type: EMAIL_ADDRESS
+ heuristic type: EMAIL_ADDRESS
+ label: Email
+ parseable name: GuestEmail
+ field signature: 658796670
+ form signature: 7987654844929240962" type="text" name="GuestEmail" id="email" value="" maxlength="128"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </li>
+ <li>
+ <label>
+ <span>
+ <strong>NOTE:</strong> Your phone number is needed to contact you for shipping-related questions and your email address will be used only to send you information about your order.
+ </span>
+ <p>
+ <span>
+ Please visit our <a>Privacy Policy</a> for more information.</span>
+ </p>
+ </label>
+ </li>
+ <input type="hidden" id="IsDefault" name="IsDefault">
+ </ul>
+ </li>
+ </ul>
+ <input type="hidden" value="True" id="CheckGuestEmail" name="CheckGuestEmail">
+ </form>
+ <input type="hidden" id="FedExShippingIsDefault" value="False">
+ <input type="hidden" id="CanadaPostIsDefault" value="False">
+ <input type="hidden" id="IsBrowserBack" value="false">
+ <ul>
+ <li>
+ <input type="radio" name="shippingAddressPanel" value="creditcard" id="shippingAddressAdd">
+ <label for="shippingAddressAdd">
+ <strong>Enter a new shipping address</strong>
+ </label>
+ <div id="checkoutShippingAddPanel">
+ </div>
+ </li>
+ </ul>
+ <div>
+ <input type="checkbox" id="makeDefaultOption" name="makeDefaultOption">
+ <label for="makeDefaultOption">Set as default credit card</label>
+ </div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en-US">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
+ <meta http-equiv="expires" content="0">
+ <meta http-equiv="pragma" content="no-cache">
+ <meta http-equiv="cache-control" content="no-cache">
+ <meta http-equiv="pragma-directive" content="no-cache">
+ <meta http-equiv="cache-directive" content="no-cache">
+ <meta name="robots" content="NOODP, NOYDIR">
+ <title>
+</title>
+ </head>
+ <body>
+ <meta name="apple-itunes-app" content="app-id=471037434">
+ <title>Office Supplies, Furniture, Technology at Offic Depot</title>
+ <meta name="description" content="Shop office supplies, furniture &amp; technology at Office Depot. For paper, ink, toner &amp; more, find trusted brands at everyday low prices.">
+ <meta name="keywords" content="office supplies, office furniture, technology, electronics">
+ <meta property="og:image" content="https://secure.www.odcdn.com/images/us/od/brand.png">
+ <form name="anonymousConfirmForm" method="post" action="https://www.officedepot.com/checkout/anonymousConfirmRouter.do" id="confirmFormId" novalidate="novalidate">
+ <input type="hidden" name="partialReg" value="false">
+ <input type="hidden" name="cartIsNotAllPickup" value="true">
+ <input type="hidden" name="step" value="bill">
+ <input type="hidden" name="orderNumber" value="914646582" id="orderNumber">
+ <input type="hidden" name="orderSubNumber" value="001" id="orderSubNumber">
+ <input type="hidden" name="linked" value="false">
+ <input type="hidden" name="billToID" value="">
+ <input type="hidden" name="requestFromPage" value="true">
+ <input type="hidden" name="nececessaryToRevalidate" id="nececessaryToRevalidate" value="false">
+ <input type="hidden" name="revalidateTrigger" id="revalidateTrigger" value="">
+ <input type="submit" name="cmd_confirm" tabindex="1">
+ <input type="hidden" name="proceedFromBill" value="true">
+ <input type="hidden" name="flowMode" id="flowMode" value="ANONYMOUS">
+ <div id="checkoutBillingV3">
+ <div>
+ <div>
+ <div>
+ <input type="hidden" name="guestEmailOptIn" value="true">
+ <input type="hidden" id="isAnonCheckout" value="true">
+ <div>
+ <input type="hidden" name="payWithSavedCard" id="payWithSavedCard" value="false">
+ <input type="hidden" id="cardTypeForMaskedValue" value="">
+ <input type="hidden" id="showCardList" value="false">
+ <div id="payWithCreditCard">
+ <div id="iFrameFields">
+ <div id="creditCardIframe">
+ <div id="cardValidationError">
+</div>
+ <div id="preventFlicker">
+ <div id="creditCardFrame">
+ <div id="iFrameExpDateFields">
+ <label>Expiration Date</label>
+ <select name="paymentFormInfo.payPageFormInfo.creditCardExpMonth"
+title="overall type: CREDIT_CARD_EXP_MONTH server type: CREDIT_CARD_EXP_MONTH heuristic type: UNKNOWN_TYPE label: Expiration Date parseable name: paymentFormInfo.payPageFormInfo.creditCardExpMonth field signature: 1909413716 form signature: 4882000530220881601" tabindex="2"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option value=" ">
+ --
+ </option>
+ <option value="01">
+ 01
+ </option>
+ <option value="02">
+ 02
+ </option>
+ <option value="03">
+ 03
+ </option>
+ <option value="04">
+ 04
+ </option>
+ <option value="05">
+ 05
+ </option>
+ <option value="06">
+ 06
+ </option>
+ <option value="07">
+ 07
+ </option>
+ <option value="08">
+ 08
+ </option>
+ <option value="09">
+ 09
+ </option>
+ <option value="10">
+ 10
+ </option>
+ <option value="11">
+ 11
+ </option>
+ <option value="12">
+ 12
+ </option>
+ </select>
+ <select name="paymentFormInfo.payPageFormInfo.creditCardExpYear"
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR server type: CREDIT_CARD_EXP_4_DIGIT_YEAR heuristic type: CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR label: Expiration Date parseable name: paymentFormInfo.payPageFormInfo.creditCardExpYear field signature: 884603578 form signature: 4882000530220881601" tabindex="3"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option value="">
+ --
+ </option>
+ <option value="17">
+ 2017
+ </option>
+ <option value="18">
+ 2018
+ </option>
+ <option value="19">
+ 2019
+ </option>
+ <option value="20">
+ 2020
+ </option>
+ <option value="21">
+ 2021
+ </option>
+ <option value="22">
+ 2022
+ </option>
+ <option value="23">
+ 2023
+ </option>
+ <option value="24">
+ 2024
+ </option>
+ <option value="25">
+ 2025
+ </option>
+ <option value="26">
+ 2026
+ </option>
+ <option value="27">
+ 2027
+ </option>
+ <option value="28">
+ 2028
+ </option>
+ <option value="29">
+ 2029
+ </option>
+ <option value="30">
+ 2030
+ </option>
+ <option value="31">
+ 2031
+ </option>
+ <option value="32">
+ 2032
+ </option>
+ <option value="33">
+ 2033
+ </option>
+ <option value="34">
+ 2034
+ </option>
+ <option value="35">
+ 2035
+ </option>
+ </select>
+ </div>
+ <div id="frameFields">
+ <input type="hidden" id="getMerchantIdPrefix" value="CKWWW">
+ <input type="hidden" id="paypageRegistrationId" name="paymentFormInfo.payPageFormInfo.payPageRegistrationId" >
+ <input type="hidden" id="bin" name="paymentFormInfo.payPageFormInfo.binRange">
+ <input type="hidden" id="merchantTxnId" name="paymentFormInfo.payPageFormInfo.merchantTxnId" value="CKWWW-201732012117">
+ <input type="hidden" id="orderId" name="paymentFormInfo.payPageFormInfo.orderId" value="914646582-001">
+ <input type="hidden" id="code" name="paymentFormInfo.payPageFormInfo.code">
+ <input type="hidden" id="responseTime" name="paymentFormInfo.payPageFormInfo.responseTime" >
+ <input type="hidden" id="message" name="paymentFormInfo.payPageFormInfo.message" size="100">
+ <input type="hidden" id="litleTxnId" name="paymentFormInfo.payPageFormInfo.litleTxnId">
+ <input type="hidden" id="type" name="paymentFormInfo.payPageFormInfo.cardType">
+ <input type="hidden" id="firstSix" name="paymentFormInfo.payPageFormInfo.firstSix">
+ <input type="hidden" id="lastFour" name="paymentFormInfo.payPageFormInfo.lastFour">
+ <input type="hidden" id="timeoutMessage" name="paymentFormInfo.payPageFormInfo.timeoutMessage" >
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="paymentFormInfo.tenderType" >
+ <input type="radio" name="paymentFormInfo.tenderType" value="CR" checked
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Credit Card parseable name: paymentFormInfo.tenderType field signature: 920121368 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+> Credit Card</label>
+ </div>
+ </div>
+ </div>
+ <div id="payWithPayPal">
+ <input type="hidden" name="paypalInfoInSession" id="paypalInfoInSession" value="false">
+ <div>
+ <label for="paymentFormInfo.tenderType" >
+ <input type="radio" name="paymentFormInfo.tenderType" value="PL"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Pay with PayPal You'll be redirected to the PayPal site to sign in and confirm your payment. You wil parseable name: paymentFormInfo.tenderType field signature: 920121368 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </label>
+ </div>
+ </div>
+ <div id="payWithMasterPass">
+ <div>
+ <label for="paymentFormInfo.tenderType" >
+ <input type="radio" name="paymentFormInfo.tenderType" value="MS"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Pay with MasterPass You'll be redirected to the MasterPass site to sign in and confirm your payment. parseable name: paymentFormInfo.tenderType field signature: 920121368 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+>
+</label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="checkoutBillingAddressSection">
+ <div id="shippingInfo">
+ <input type="hidden" name="addrsForm[2].firstName" value="Tester" id="firstName-2">
+ <input type="hidden" name="addrsForm[2].lastName" value="Mo" id="lastName-2">
+ <input type="hidden" name="addrsForm[2].shiptoName" value="Mozilla" id="shiptoName-2">
+ <input type="hidden" name="addrsForm[2].address1" value="331 E. Evelyn Avenue" id="address1-2">
+ <input type="hidden" name="addrsForm[2].address2" value="" id="address2-2">
+ <input type="hidden" name="addrsForm[2].city" value="MOUNTAIN VIEW" id="city-2">
+ <input type="hidden" name="addrsForm[2].state" value="CA" id="state-2">
+ <input type="hidden" name="addrsForm[2].postalCode1" value="94041" id="postalCode1-2">
+ <input type="hidden" name="addrsForm[2].country" value="USA" id="country-2">
+ <input type="hidden" name="addrsForm[2].phoneNumber1" value="650" id="phoneNumber1-2">
+ <input type="hidden" name="addrsForm[2].phoneNumber2" value="903" id="phoneNumber2-2">
+ <input type="hidden" name="addrsForm[2].phoneNumber3" value="0800" id="phoneNumber3-2">
+ <input type="hidden" name="addrsForm[2].phoneNumber4" value="0800" id="phoneNumber4-2">
+ <input type="hidden" name="addrsForm[2].email" value="formautofilltester@gmail.com" id="email-2">
+ <input type="hidden" name="shippingEmailPreferences.optInSelected" value="true">
+ </div>
+ <div id="billingInfo">
+ <div>
+ <label>
+ <input type="checkbox" name="sameAsBilling" id="billingAddressSameAsShipping" checked tabindex="4"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Same as Shipping parseable name: sameAsBilling field signature: 2684212655 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+> Same as Shipping</label>
+ </div>
+ <div id="reg_billingInfo">
+ <div id="anonymousBillingInfoForm">
+ <input type="hidden" id="country-0" name="addrsForm[0].country" value="USA">
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="firstName-0">
+<em>*</em>First Name:</label>
+ <input type="text" id="firstName-0" name="addrsForm[0].firstName" value="" maxlength="30"
+title="overall type: CREDIT_CARD_NAME_FIRST server type: NAME_FIRST heuristic type: CREDIT_CARD_NAME_FIRST label: *First Name: parseable name: addrsForm[0].firstName field signature: 923482701 form signature: 4882000530220881601"
+autofill-prediction="CREDIT_CARD_NAME_FIRST"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="lastName-0">
+<em>*</em>Last Name:</label>
+ <input type="text" id="lastName-0" name="addrsForm[0].lastName" value="" maxlength="30"
+title="overall type: CREDIT_CARD_NAME_LAST server type: NAME_LAST heuristic type: CREDIT_CARD_NAME_LAST label: *Last Name: parseable name: addrsForm[0].lastName field signature: 3226352217 form signature: 4882000530220881601"
+autofill-prediction="CREDIT_CARD_NAME_LAST"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <label for="shipToName-0">Company Name:</label>
+ <input type="text" id="shipToName-0" name="addrsForm[0].shiptoName" value="" maxlength="30"
+title="overall type: COMPANY_NAME server type: COMPANY_NAME heuristic type: COMPANY_NAME label: Company Name: parseable name: addrsForm[0].shiptoName field signature: 2508018426 form signature: 4882000530220881601"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ <div>
+ <label for="address1-0">
+<em>*</em>Address:</label>
+ <input type="text" id="address1-0" name="addrsForm[0].address1" value="" maxlength="25"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: *Address: parseable name: addrsForm[0].address1 field signature: 3908367322 form signature: 4882000530220881601"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <div>
+ <label for="address2-0">Address Line 2: <span>(optional)</span>
+</label>
+ <input type="text" id="address2-0" name="addrsForm[0].address2" value="" maxlength="25"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Address Line 2: (optional) parseable name: addrsForm[0].address2 field signature: 3866764654 form signature: 4882000530220881601"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="postalCode1-0">
+<em>*</em>Postal Code:</label>
+ <input type="text" id="postalCode1-0" name="addrsForm[0].postalCode1" value="" maxlength="9"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: *Postal Code: parseable name: addrsForm[0].postalCode1 field signature: 1708240695 form signature: 4882000530220881601"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div >
+ <label for="checkoutCityAndState">
+<em>*</em>City & State</label>
+ <select id="checkoutCityAndState"
+title="overall type: ADDRESS_HOME_CITY server type: NO_SERVER_DATA heuristic type: ADDRESS_HOME_CITY label: *City &amp; State parseable name: checkoutCityAndState field signature: 3899416585 form signature: 4882000530220881601"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="city-0">
+<em>*</em>City:</label>
+ <input type="text" id="city-0" name="addrsForm[0].city" value=""
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: *City: parseable name: addrsForm[0].city field signature: 1634404945 form signature: 4882000530220881601"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ </div>
+ <div>
+ <div >
+ <label for="state-0">
+<em>*</em>State:</label>
+ <select name="addrsForm[0].state" id="state-0" size="1"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: *State: parseable name: addrsForm[0].state field signature: 3377657622 form signature: 4882000530220881601"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="{blank}"> Select a state
+ </option>
+ <option value="AK"> AK - Alaska
+ </option>
+ <option value="AL"> AL - Alabama
+ </option>
+ <option value="AR"> AR - Arkansas
+ </option>
+ <option value="AZ"> AZ - Arizona
+ </option>
+ <option value="CA"> CA - California
+ </option>
+ <option value="CO"> CO - Colorado
+ </option>
+ <option value="CT"> CT - Connecticut
+ </option>
+ <option value="DC"> DC - District of Columbia
+ </option>
+ <option value="DE"> DE - Delaware
+ </option>
+ <option value="FL"> FL - Florida
+ </option>
+ <option value="GA"> GA - Georgia
+ </option>
+ <option value="HI"> HI - Hawaii
+ </option>
+ <option value="IA"> IA - Iowa
+ </option>
+ <option value="ID"> ID - Idaho
+ </option>
+ <option value="IL"> IL - Illinois
+ </option>
+ <option value="IN"> IN - Indiana
+ </option>
+ <option value="KS"> KS - Kansas
+ </option>
+ <option value="KY"> KY - Kentucky
+ </option>
+ <option value="LA"> LA - Louisiana
+ </option>
+ <option value="MA"> MA - Massachusetts
+ </option>
+ <option value="MD"> MD - Maryland
+ </option>
+ <option value="ME"> ME - Maine
+ </option>
+ <option value="MI"> MI - Michigan
+ </option>
+ <option value="MN"> MN - Minnesota
+ </option>
+ <option value="MO"> MO - Missouri
+ </option>
+ <option value="MS"> MS - Mississippi
+ </option>
+ <option value="MT"> MT - Montana
+ </option>
+ <option value="NC"> NC - North Carolina
+ </option>
+ <option value="ND"> ND - North Dakota
+ </option>
+ <option value="NE"> NE - Nebraska
+ </option>
+ <option value="NH"> NH - New Hampshire
+ </option>
+ <option value="NJ"> NJ - New Jersey
+ </option>
+ <option value="NM"> NM - New Mexico
+ </option>
+ <option value="NV"> NV - Nevada
+ </option>
+ <option value="NY"> NY - New York
+ </option>
+ <option value="OH"> OH - Ohio
+ </option>
+ <option value="OK"> OK - Oklahoma
+ </option>
+ <option value="OR"> OR - Oregon
+ </option>
+ <option value="PA"> PA - Pennsylvania
+ </option>
+ <option value="PR"> PR - Puerto Rico
+ </option>
+ <option value="RI"> RI - Rhode Island
+ </option>
+ <option value="SC"> SC - South Carolina
+ </option>
+ <option value="SD"> SD - South Dakota
+ </option>
+ <option value="TN"> TN - Tennessee
+ </option>
+ <option value="TX"> TX - Texas
+ </option>
+ <option value="UT"> UT - Utah
+ </option>
+ <option value="VA"> VA - Virginia
+ </option>
+ <option value="VI"> VI - US Virgin Islands
+ </option>
+ <option value="VT"> VT - Vermont
+ </option>
+ <option value="WA"> WA - Washington
+ </option>
+ <option value="WI"> WI - Wisconsin
+ </option>
+ <option value="WV"> WV - West Virginia
+ </option>
+ <option value="WY"> WY - Wyoming
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label>
+<em>*</em>Phone:</label>
+ <input type="tel" id="phoneNumber1-0" name="addrsForm[0].phoneNumber1" value="" maxlength="3"
+title="overall type: PHONE_HOME_CITY_CODE server type: PHONE_HOME_CITY_CODE heuristic type: PHONE_HOME_CITY_CODE label: *Phone: parseable name: addrsForm[0].phoneNumber1 field signature: 1254557631 form signature: 4882000530220881601"
+autofill-prediction="PHONE_HOME_CITY_CODE"
+>
+ <input type="tel" id="phoneNumber2-0" name="addrsForm[0].phoneNumber2" value="" maxlength="3"
+title="overall type: PHONE_HOME_NUMBER server type: PHONE_HOME_NUMBER heuristic type: PHONE_HOME_NUMBER label: *Phone: parseable name: addrsForm[0].phoneNumber2 field signature: 1999321122 form signature: 4882000530220881601"
+autofill-prediction="PHONE_HOME_NUMBER"
+>
+ <input type="tel" id="phoneNumber3-0" name="addrsForm[0].phoneNumber3" value="" maxlength="4"
+title="overall type: PHONE_HOME_NUMBER server type: PHONE_HOME_NUMBER heuristic type: PHONE_HOME_NUMBER label: *Phone: parseable name: addrsForm[0].phoneNumber3 field signature: 348537713 form signature: 4882000530220881601"
+autofill-prediction="PHONE_HOME_NUMBER"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="phoneNumber4-0">Ext</label>
+ <input type="tel" id="phoneNumber4-0" name="addrsForm[0].phoneNumber4" value="" maxlength="4"
+title="overall type: PHONE_HOME_NUMBER server type: PHONE_HOME_NUMBER heuristic type: PHONE_HOME_EXTENSION label: Ext parseable name: addrsForm[0].phoneNumber4 field signature: 3060772033 form signature: 4882000530220881601"
+autofill-prediction="PHONE_HOME_NUMBER"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div id="addressChangeEmailConfirm">
+ <label for="email-0">
+<em>*</em>Email Address:</label>
+ <input type="email" id="email-0" name="addrsForm[0].email" value="" maxlength="40"
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: *Email Address: parseable name: addrsForm[0].email field signature: 1389763646 form signature: 4882000530220881601"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="hidden" name="billingEmailPreferences.emailHtml" value="true">
+ <div>
+ <input type="hidden" name="billingEmailPreferences.optInSelected" value="true">
+<a target="_blank">Privacy Policy</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div id="checkoutBillLoyalty">
+ <div id="rewardsSections">
+ <div id="noWorkLifeRewards">
+ <div>
+ <div>
+ <div>
+ <input type="text" name="loyaltyID"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: UNKNOWN_TYPE label: Apply parseable name: loyaltyID field signature: 1222391720 form signature: 4882000530220881601" maxlength="10" value=""
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+>
+ <p>
+<a
+ >Member number lookup</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="checkoutBillingGiftCards">
+ <div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="gcInput">Gift card or certificate number</label>
+ <input type="tel" name="paymentFormInfo.storedValueCardNumber" maxlength="19" value="" id="gcInput"
+title="overall type: CREDIT_CARD_NUMBER server type: CREDIT_CARD_NUMBER heuristic type: UNKNOWN_TYPE label: Gift card or certificate number parseable name: paymentFormInfo.storedValueCardNumber field signature: 2610516022 form signature: 4882000530220881601"
+autofill-prediction="CREDIT_CARD_NUMBER"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="gcPinInput">PIN</label>
+ <input type="tel" name="paymentFormInfo.storedValueCardPin" maxlength="4" value="" id="gcPinInput"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: PIN parseable name: paymentFormInfo.storedValueCardPin field signature: 3801610036 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE">
+<img src="./Office%20Supplies,%20Furniture,%20Technology%20at%20Office%20Depot%20-%20Payment_files/info-blue.png" alt="Need Help?" title="Need Help?" id="giftCardPinTooltipTarget"
+>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <input type="hidden" id="couponCount" value="0">
+ <input type="hidden" id="invalidCouponNumber" value="">
+ <input type="hidden" id="couponInWhichStep" value="checkoutV2">
+ <input type="hidden" id="couponRemove" value="Remove Coupon">
+ <input type="hidden" id="close" value="Close">
+ <input type="hidden" id="invalidCouponCode" value="&lt;p&gt;Invalid Coupon Code&lt;/p&gt;">
+ <input type="hidden" id="validCouponCode" value="&lt;p&gt;Coupon Code Applied&lt;/p&gt;">
+ <input type="hidden" id="validCouponPrompt" value="This coupon has been applied to your order.">
+ <input type="hidden" id="couponRemoveFailHeader" value="Remove Coupon Fail">
+ <input type="hidden" name="offer.x" value="y">
+ <div>
+ <div id="couponDialog"
+title="Coupon Offer">
+ <div>
+ <div>
+ <ul>
+ <li>
+ <input type="button" value="Skip and Continue">
+ </li>
+ <li>
+ <input type="button" value="See Offer Details">
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="referralCode">Coupon Code:</label>
+ <input type="tel" name="referralCode" id="referralCode" size="14" maxlength="14"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Coupon Code: parseable name: referralCode field signature: 2314309716 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="hidden" name="customLabelsPermissionsForm.requestor" value="confirm">
+ <input type="hidden" name="modifyFormDataWithBackendData" value="true">
+ <div id="checkoutBillingAdditionalInfo">
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="sourceCode">Catalog/source code, Federal government code <img src="./Office%20Supplies,%20Furniture,%20Technology%20at%20Office%20Depot%20-%20Payment_files/info-blue.png" alt="What is a Catalog or Source Code, Federal Government Code?"
+title="What is a Catalog or Source Code, Federal Government Code?">
+ </label>
+ <input type="text" name="sourceCode" maxlength="5" size="22" autocomplete="" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Catalog/source code, Federal government code parseable name: sourceCode field signature: 1650657570 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label>Customer PO#</label>
+ <input type="text" name="poName" maxlength="22" size="22" autocomplete="" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Customer PO# parseable name: poName field signature: 330999673 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label>Comments</label>
+ <p>
+<label>Informational purposes only. Not utilized by our delivery carriers.</label>
+ </p>
+ <textarea name="commentText" cols="30" rows="3" maxlength="87"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Comments Informational purposes only. Not utilized by our delivery carriers. parseable name: commentText field signature: 2293147818 form signature: 4882000530220881601"
+autofill-prediction="UNKNOWN_TYPE"
+>
+</textarea>
+ <span id="">87</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="paymentsButtonsMainBilling">
+ <input type="hidden" id="showPayPalCheckoutButton_toggleCheck" name="showPayPalCheckoutButton_toggleCheck" value="true">
+ <input type="hidden" id="showMasterPassCheckoutButton_toggleCheck" name="showMasterPassCheckoutButton_toggleCheck" value="true">
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <table>
+ <tbody>
+ <tr>
+ <td colspan="4">
+ <input type="hidden" name="cartRow[0].cartEntryId" value="0">
+ <input type="hidden" name="cartRow[0].minQty" value="0">
+ <input type="hidden" name="cartRow[0].originalQty" value="0">
+ <input type="hidden" name="cartRow[0].skuNoEffort" value="510493">
+ <input type="hidden" name="cartRow[0].qtyMinimumLimitation" value="1">
+ <input type="hidden" name="cartRow[0].qtyIncrementLimitation" value="1">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div id="paymentsOrderSummary">
+ <input type="hidden" id="showPayPalCheckoutButton_toggleCheck" name="showPayPalCheckoutButton_toggleCheck" value="true">
+ <input type="hidden" id="showMasterPassCheckoutButton_toggleCheck" name="showMasterPassCheckoutButton_toggleCheck" value="true">
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en-US">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
+ <meta http-equiv="expires" content="0">
+ <meta http-equiv="pragma" content="no-cache">
+ <meta http-equiv="cache-control" content="no-cache">
+ <meta http-equiv="pragma-directive" content="no-cache">
+ <meta http-equiv="cache-directive" content="no-cache">
+ <meta name="robots" content="NOODP, NOYDIR">
+ <title>
+</title>
+ </head>
+ <body>
+ <meta name="apple-itunes-app" content="app-id=471037434">
+ <title>Office Supplies, Furniture, Technology at Office Depot</title>
+ <meta name="description" content="Shop office supplies, furniture &amp; technology at Office Depot. For paper, ink, toner &amp; more, find trusted brands at everyday low prices.">
+ <meta name="keywords" content="office supplies, office furniture, technology, electronics">
+ <meta property="og:image" content="https://secure.www.odcdn.com/images/us/od/brand.png">
+ <form name="anonymousConfirmForm" method="post" action="https://www.officedepot.com/checkout/anonymousConfirmRouter.do" id="confirmFormId" novalidate="novalidate">
+ <input type="hidden" name="partialReg" value="false">
+<input type="hidden" name="cartIsNotAllPickup" value="true">
+<input type="hidden" name="returnurl" value="/checkout/checkout/anonymousConfirmRouter.do">
+ <div id="shipPageV2">
+ <div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <div id="reg_shippingInfo">
+ <input id="skipGroup1ShippingAddress" type="hidden" name="skipGroup1ShippingAddress" value="false">
+ <div>
+ <input type="hidden" id="country-2" name="addrsForm[2].country" value="USA">
+ <div>
+ <div>
+ <div>
+ <div >
+ <label for="firstName-2" >
+ <em>*</em>First Name:</label>
+ <input type="text" id="firstName-2" name="addrsForm[2].firstName" value="" maxlength="30" tabindex="1"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: *First Name: parseable name: addrsForm[2].firstName field signature: 3337773590 form signature: 5001876119589580889"
+autofill-prediction="NAME_FIRST"
+>
+ </div>
+ </div>
+ <div>
+ <div >
+ <label for="lastName-2">
+ <em>*</em>Last Name:</label>
+ <input type="text" id="lastName-2" name="addrsForm[2].lastName" value="" maxlength="30" tabindex="2"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: *Last Name: parseable name: addrsForm[2].lastName field signature: 3075576638 form signature: 5001876119589580889"
+autofill-prediction="NAME_LAST"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <label for="shipToName-2"> Company Name:</label>
+ <input type="text" id="shipToName-2" name="addrsForm[2].shiptoName" value="" maxlength="30" tabindex="3"
+title="overall type: COMPANY_NAME server type: COMPANY_NAME heuristic type: COMPANY_NAME label: Company Name: parseable name: addrsForm[2].shiptoName field signature: 2052742641 form signature: 5001876119589580889"
+autofill-prediction="COMPANY_NAME"
+>
+ </div>
+ <div >
+ <label for="address1-2">
+ <em>*</em>Address:</label>
+ <input type="text" id="address1-2" name="addrsForm[2].address1" value="" maxlength="25" tabindex="4"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: *Address: parseable name: addrsForm[2].address1 field signature: 2660215956 form signature: 5001876119589580889"
+autofill-prediction="ADDRESS_HOME_LINE1"
+>
+ </div>
+ <div>
+ <label for="address2-2"> Address Line 2:
+ <span>(optional)</span>
+</label>
+ <input type="text" id="address2-2" name="addrsForm[2].address2" value="" maxlength="25" tabindex="5"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Address Line 2: (optional) parseable name: addrsForm[2].address2 field signature: 2293911247 form signature: 5001876119589580889"
+autofill-prediction="ADDRESS_HOME_LINE2"
+>
+ </div>
+ <div>
+ <div>
+ <div >
+ <label for="postalCode1-2">
+ <em>*</em>Postal Code:</label>
+ <input type="text" id="postalCode1-2" name="addrsForm[2].postalCode1" value="" maxlength="9" tabindex="6"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: *Postal Code: parseable name: addrsForm[2].postalCode1 field signature: 1044898225 form signature: 5001876119589580889"
+autofill-prediction="ADDRESS_HOME_ZIP"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div >
+ <label for="checkoutCityAndState">
+<em>*</em>
+</em>City & State</label>
+ <select id="checkoutCityAndState"
+title="overall type: ADDRESS_HOME_CITY server type: NO_SERVER_DATA heuristic type: ADDRESS_HOME_CITY label: *City &amp; State parseable name: checkoutCityAndState field signature: 3899416585 form signature: 5001876119589580889"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ <option value="0"> MOUNTAIN VIEW, CA
+ </option>
+ <option value="other"> Other City and State
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div >
+ <label for="city-2">
+ <em>*</em>City:</label>
+ <input type="text" id="city-2" name="addrsForm[2].city" value=""
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: *City: parseable name: addrsForm[2].city field signature: 2341281094 form signature: 5001876119589580889"
+autofill-prediction="ADDRESS_HOME_CITY"
+>
+ </div>
+ </div>
+ <div>
+ <div >
+ <label for="state-2">
+ <em>*</em>State:</label>
+ <select name="addrsForm[2].state" id="state-2" size="1"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: *State: parseable name: addrsForm[2].state field signature: 3265256938 form signature: 5001876119589580889"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="{blank}"> Select a state
+ </option>
+ <option value="AK"> AK - Alaska
+ </option>
+ <option value="AL"> AL - Alabama
+ </option>
+ <option value="AR"> AR - Arkansas
+ </option>
+ <option value="AZ"> AZ - Arizona
+ </option>
+ <option value="CA"> CA - California
+ </option>
+ <option value="CO"> CO - Colorado
+ </option>
+ <option value="CT"> CT - Connecticut
+ </option>
+ <option value="DC"> DC - District of Columbia
+ </option>
+ <option value="DE"> DE - Delaware
+ </option>
+ <option value="FL"> FL - Florida
+ </option>
+ <option value="GA"> GA - Georgia
+ </option>
+ <option value="HI"> HI - Hawaii
+ </option>
+ <option value="IA"> IA - Iowa
+ </option>
+ <option value="ID"> ID - Idaho
+ </option>
+ <option value="IL"> IL - Illinois
+ </option>
+ <option value="IN"> IN - Indiana
+ </option>
+ <option value="KS"> KS - Kansas
+ </option>
+ <option value="KY"> KY - Kentucky
+ </option>
+ <option value="LA"> LA - Louisiana
+ </option>
+ <option value="MA"> MA - Massachusetts
+ </option>
+ <option value="MD"> MD - Maryland
+ </option>
+ <option value="ME"> ME - Maine
+ </option>
+ <option value="MI"> MI - Michigan
+ </option>
+ <option value="MN"> MN - Minnesota
+ </option>
+ <option value="MO"> MO - Missouri
+ </option>
+ <option value="MS"> MS - Mississippi
+ </option>
+ <option value="MT"> MT - Montana
+ </option>
+ <option value="NC"> NC - North Carolina
+ </option>
+ <option value="ND"> ND - North Dakota
+ </option>
+ <option value="NE"> NE - Nebraska
+ </option>
+ <option value="NH"> NH - New Hampshire
+ </option>
+ <option value="NJ"> NJ - New Jersey
+ </option>
+ <option value="NM"> NM - New Mexico
+ </option>
+ <option value="NV"> NV - Nevada
+ </option>
+ <option value="NY"> NY - New York
+ </option>
+ <option value="OH"> OH - Ohio
+ </option>
+ <option value="OK"> OK - Oklahoma
+ </option>
+ <option value="OR"> OR - Oregon
+ </option>
+ <option value="PA"> PA - Pennsylvania
+ </option>
+ <option value="PR"> PR - Puerto Rico
+ </option>
+ <option value="RI"> RI - Rhode Island
+ </option>
+ <option value="SC"> SC - South Carolina
+ </option>
+ <option value="SD"> SD - South Dakota
+ </option>
+ <option value="TN"> TN - Tennessee
+ </option>
+ <option value="TX"> TX - Texas
+ </option>
+ <option value="UT"> UT - Utah
+ </option>
+ <option value="VA"> VA - Virginia
+ </option>
+ <option value="VI"> VI - US Virgin Islands
+ </option>
+ <option value="VT"> VT - Vermont
+ </option>
+ <option value="WA"> WA - Washington
+ </option>
+ <option value="WI"> WI - Wisconsin
+ </option>
+ <option value="WV"> WV - West Virginia
+ </option>
+ <option value="WY"> WY - Wyoming
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label>
+ <em>*</em>Phone:</label>
+ <input type="tel" id="phoneNumber1-2" name="addrsForm[2].phoneNumber1" value="" maxlength="3"
+title="overall type: PHONE_HOME_CITY_CODE server type: PHONE_HOME_CITY_CODE heuristic type: PHONE_HOME_CITY_CODE label: *Phone: parseable name: addrsForm[2].phoneNumber1 field signature: 3051888398 form signature: 5001876119589580889" tabindex="7"
+autofill-prediction="PHONE_HOME_CITY_CODE"
+>
+ <input type="tel" id="phoneNumber2-2" name="addrsForm[2].phoneNumber2" value="" maxlength="3"
+title="overall type: PHONE_HOME_NUMBER server type: PHONE_HOME_NUMBER heuristic type: PHONE_HOME_NUMBER label: *Phone: parseable name: addrsForm[2].phoneNumber2 field signature: 4001233923 form signature: 5001876119589580889" tabindex="8"
+autofill-prediction="PHONE_HOME_NUMBER"
+>
+ <input type="tel" id="phoneNumber3-2" name="addrsForm[2].phoneNumber3" value="" maxlength="4"
+title="overall type: PHONE_HOME_NUMBER server type: PHONE_HOME_NUMBER heuristic type: PHONE_HOME_NUMBER label: *Phone: parseable name: addrsForm[2].phoneNumber3 field signature: 3507119292 form signature: 5001876119589580889" tabindex="9"
+autofill-prediction="PHONE_HOME_NUMBER"
+>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="phoneNumber4-2"> Ext</label>
+ <input type="tel" id="phoneNumber4-2" name="addrsForm[2].phoneNumber4" value="" maxlength="4" tabindex="10"
+title="overall type: PHONE_HOME_NUMBER server type: PHONE_HOME_NUMBER heuristic type: PHONE_HOME_EXTENSION label: Ext parseable name: addrsForm[2].phoneNumber4 field signature: 2592995828 form signature: 5001876119589580889"
+autofill-prediction="PHONE_HOME_NUMBER"
+>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div id="addressChangeEmailConfirm" >
+ <label for="email-2">
+ <em>*</em>Email Address:</label>
+ <input type="email" id="email-2" name="addrsForm[2].email" value="" maxlength="40" tabindex="11"
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: *Email Address: parseable name: addrsForm[2].email field signature: 3353412459 form signature: 5001876119589580889"
+autofill-prediction="EMAIL_ADDRESS"
+>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="checkbox" id="guestEmailOptIn" name="guestEmailOptIn" checked="checked" tabindex="12"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Send me exclusive coupons and special offers to my inbox. parseable name: guestEmailOptIn field signature: 2624588538 form signature: 5001876119589580889"
+autofill-prediction="UNKNOWN_TYPE">
+<input type="hidden" name="guestEmailOptIn" value="false">
+<label for="guestEmailOptIn"
+>Send me exclusive coupons and special offers to my inbox.</label>
+ </div>
+ <input type="hidden" name="shippingEmailPreferences.emailHtml" value="true">
+<input type="hidden" name="shippingEmailPreferences.optInSelected" value="true">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <table>
+ <tbody>
+ <tr>
+ <td colspan="4">
+<input type="hidden" name="cartRow[0].cartEntryId" value="0">
+<input type="hidden" name="cartRow[0].minQty" value="0">
+ <input type="hidden" name="cartRow[0].originalQty" value="0">
+<input type="hidden" name="cartRow[0].skuNoEffort" value="510493">
+<input type="hidden" name="cartRow[0].qtyMinimumLimitation" value="1">
+<input type="hidden" name="cartRow[0].qtyIncrementLimitation" value="1">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <input type="hidden" name="step" value="ship">
+<input type="hidden" name="orderNumber" value="914646582">
+<input type="hidden" name="orderSubNumber" value="001">
+<input type="hidden" name="sameAsBilling" value="true">
+ <input type="hidden" name="linked" value="false">
+<input type="hidden" name="billToID" value="">
+<input type="hidden" name="nececessaryToRevalidate" id="nececessaryToRevalidate" value="false">
+ <input type="hidden" name="revalidateTrigger" id="revalidateTrigger" value="">
+<input type="hidden" name="group1Error" id="group1Error" value="false">
+<input type="hidden" name="flowMode" id="flowMode" value="ANONYMOUS">
+ <div id="skipGroupOne">
+ <div>
+ <input type="submit" value="Edit" name="cmd_edit" id="editShipping"
+title="Edit">
+<button type="submit" name="cmd_confirm" id="continue">Continue</button>
+ </div>
+ </div>
+ </div>
+ </form>
+ <input type="hidden" name="enableGoogleAddr" id="enableGoogleAddr" value="true">
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en-US">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
+ <meta http-equiv="expires" content="0">
+ <meta http-equiv="pragma" content="no-cache">
+ <meta http-equiv="cache-control" content="no-cache">
+ <meta http-equiv="pragma-directive" content="no-cache">
+ <meta http-equiv="cache-directive" content="no-cache">
+ <meta name="robots" content="NOODP, NOYDIR">
+ <title>
+</title>
+ </head>
+ <body>
+ <meta name="apple-itunes-app" content="app-id=471037434">
+ <title>Office Supplies, Furniture, Technology at Office Depot</title>
+ <meta name="description" content="Shop office supplies, furniture &amp; technology at Office Depot. For paper, ink, toner &amp; more, find trusted brands at everyday low prices.">
+ <meta name="keywords" content="office supplies, office furniture, technology, electronics">
+ <meta property="og:image" content="https://secure.www.odcdn.com/images/us/od/brand.png">
+ <form name="loginForm" method="post" action="https://www.officedepot.com/account/loginAccountSet.do" autocomplete="off" id="loginForm">
+ <input type="hidden" name="confirmationRequired" value="false">
+ <input type="hidden" name="requestor" value="accountSummary">
+ <input type="hidden" name="loginDestination" value="">
+ <input type="hidden" id="isLoginFromRewardsModal" name="isLoginFromRewardsModal">
+ <input type="hidden" name="reqLevel" value="ACCOUNT">
+ <div>
+ <label for="loginName-0">Login name or email address</label>
+ <label>Logging in as a different user may cause pricing changes</label>
+ <input type="text" name="loginName" maxlength="100" size="10" autocomplete="" value="" id="loginName-0" tabindex="1">
+ </div>
+ <div>
+ <label for="loginPassword">Password</label>
+ <input type="password" name="password" maxlength="50" size="10" value="" id="loginPassword" tabindex="2">
+ <span id="forgotPasswordLink">
+<a >Forgot login name/password?</a>
+</span>
+ </div>
+ <div>
+ <input type="checkbox" name="autoLogin" tabindex="3" value="on"> Keep me logged in
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta property="fb:app_id" content="105047196722" />
+ <meta property="fb:page_id" content="23797290954" />
+ <meta name="format-detection" content="telephone=no" />
+ <meta name="viewport" content="width=919" />
+ <title>Payment Method</title>
+ </head>
+ <body>
+ <form id="frmEditPaymentMethod" name="frmEditPaymentMethod" method="post" action="https://www.qvc.com/webapp/wcs/stores/servlet/NPOOrderAddPaymentMethods">
+ <input type="hidden" name="csrfToken" value="M1E1cTBwME9saUVZaU9iNUdJVnZqSm9JQThHM3gwSUt0elROSE9oSDJaST0=" />
+ <input type="hidden" name="ccId" value="" />
+ <input type="hidden" name="payMethodExpireMonth" value="" />
+ <input type="hidden" name="payMethodExpireYear" value="" />
+ <input type="hidden" name="ccDtime" value="" />
+ <input type="hidden" name="payMethodNewCard" value="N" />
+ <input type="hidden" name="payMethodExpiryEdited" value="N" />
+ <input type="hidden" name="payMethodCVV" value="" />
+ <input type="hidden" name="orderId" value="476661567" />
+ <input type="hidden" name="langId" value="-1" />
+ <input type="hidden" name="catalogId" value="10151" />
+ <input type="hidden" name="storeId" value="10251" />
+ <input type="hidden" name="dummydata" value="" />
+ <input type="hidden" id="currentPaymentMethod" name="currentPaymentMethod" value="VI" />
+ <input type="hidden" name="checkoutStep" value="3" />
+ <input type="hidden" name="fromPaymentPage" value="Y" />
+ <input type="hidden" name="URL" value="" />
+ <input type="hidden" id="addAnotherGC" name="addAnotherGC" value="N" />
+ <input type="hidden" name="BMLFilePath" id="BMLFilePath" value="https://www.qvc.com/wcsstore/US/content/html/popups/BillMeLaterTermsandConditions.html" />
+ <div id="divTotalPurchaseSummary">
+ <fieldset>
+ <label id="lblTotalPurchaseAmount" for="spanTotalPurchaseAmount">Total Purchase
+ <span>(including tax and S&amp;H)</span>:</label>
+ </fieldset>
+ </div>
+ <div id="divEasyPayOptions">
+ <fieldset>
+ <ul>
+ <li>
+ <input type="radio" value="Z" id="rb1PaymentsItem_1" name="rbItemEasyPayOption_1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: 4 Easy Pays of $39.74 parseable name: rbItemEasyPayOption_1 field signature: 915844214 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rb1PaymentsItem_1">4 Easy Pays of $39.74</label>
+ </li>
+ <li>
+ <input type="radio" value="N" id="rb2PaymentsItem_1" name="rbItemEasyPayOption_1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: 1 payment of $158.96 parseable name: rbItemEasyPayOption_1 field signature: 915844214 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rb2PaymentsItem_1">1 payment of $158.96</label>
+ </li>
+ </ul>
+ <input type="hidden" name="hItemEasyPayOptionPayMthd_1" id="hItemEasyPayOptionPayMthd_1" value="QVAXCBDCSIMCVIBL " />
+ <input type="hidden" name="orderItemId_1" id="orderItemId_1" value="660521668" />
+ </fieldset>
+ </div>
+ <div id="divNewPaymentMethod">
+ <input id="ccId_1" type="hidden" name="ccId_1" value="" />
+ <table id="tblNewPaymentMethod" border="0" cellspacing="0" cellpadding="0" width="100%" summary="Payment methods available for Checkout">
+ <tbody>
+ <tr id="trBillMeLaterRow-1">
+ <td>
+ <input type="radio" name="rbNewPaymentMethod" id="rbBillLater" value="rbBillLater"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: PayPal Credit parseable name: rbNewPaymentMethod field signature: 95492298 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rbBillLater">PayPal Credit</label>
+ <span>(formerly Bill Me Later®)</span>
+ </td>
+ </tr>
+ <tr id="trBillMeLaterRow-2">
+ <td colspan="5">
+ <div id="divBillMeLater">
+ <fieldset>
+ <label id="lblPrimaryPhone" for="txtPrimaryPhone">Home Phone:</label>
+ <input id="txtPrimaryPhone" type="tel" name="txtPrimaryPhone" autocomplete="off" autocorrect="off" value="" maxlength="14" size="14"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: Home Phone: parseable name: txtPrimaryPhone field signature: 918983855 form signature: 12190733459375771907"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+/>
+ </fieldset>
+ <fieldset>
+ <label id="lblEmailAddress" for="txtEmailAddress">Email Address:</label>
+ <input id="txtEmailAddress" type="email" name="txtEmailAddress" autocomplete="on" autocorrect="off" value=""
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: Email Address: parseable name: txtEmailAddress field signature: 653947670 form signature: 12190733459375771907"
+autofill-prediction="EMAIL_ADDRESS"
+/>
+ </fieldset>
+ <fieldset>
+ <label id="lblSsn" for="txtSsn">Social Security Number:</label>XXX-XX-<input id="txtSsn" type="text" pattern="[0-9]*" autocomplete="off" autocorrect="off" maxlength="4" size="4" name="txtLast4SSN" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Social Security Number: parseable name: txtLast4SSN field signature: 598258955 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </fieldset>
+ <fieldset>
+ <label id="lblDateOfBirthMonth" for="selDobMonth">Date of Birth:</label>
+ <select id="selDobMonth" name="dobMonth" size="1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Date of Birth: parseable name: dobMonth field signature: 3916402925 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="month" selected="selected">Month</option>
+ <option value="01">January</option>
+ <option value="02">February</option>
+ <option value="03">March</option>
+ <option value="04">April</option>
+ <option value="05">May</option>
+ <option value="06">June</option>
+ <option value="07">July</option>
+ <option value="08">August</option>
+ <option value="09">September</option>
+ <option value="10">October</option>
+ <option value="11">November</option>
+ <option value="12">December</option>
+ </select>
+ <label id="lblDateOfBirthDay" for="selDobDay">Day of Birth:</label>
+ <select id="selDobDay" name="dobDay" size="1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Day of Birth: parseable name: dobDay field signature: 4127787517 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="day" selected="selected">Day</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ <option value="8">8</option>
+ <option value="9">9</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ <option value="24">24</option>
+ <option value="25">25</option>
+ <option value="26">26</option>
+ <option value="27">27</option>
+ <option value="28">28</option>
+ <option value="29">29</option>
+ <option value="30">30</option>
+ <option value="31">31</option>
+ </select>
+ <label id="lblDateOfBirthYear" for="selDobYear">Year of Birth:</label>
+ <select id="selDobYear" name="dobYear" size="1"
+title="overall type: COMPANY_NAME server type: COMPANY_NAME heuristic type: UNKNOWN_TYPE label: Year of Birth: parseable name: dobYear field signature: 3750696607 form signature: 12190733459375771907"
+autofill-prediction="COMPANY_NAME"
+>
+ <option value="year" selected="selected">Year</option>
+ <option value="1927">1927</option>
+ <option value="1928">1928</option>
+ <option value="1929">1929</option>
+ <option value="1930">1930</option>
+ <option value="1931">1931</option>
+ <option value="1932">1932</option>
+ <option value="1933">1933</option>
+ <option value="1934">1934</option>
+ <option value="1935">1935</option>
+ <option value="1936">1936</option>
+ <option value="1937">1937</option>
+ <option value="1938">1938</option>
+ <option value="1939">1939</option>
+ <option value="1940">1940</option>
+ <option value="1941">1941</option>
+ <option value="1942">1942</option>
+ <option value="1943">1943</option>
+ <option value="1944">1944</option>
+ <option value="1945">1945</option>
+ <option value="1946">1946</option>
+ <option value="1947">1947</option>
+ <option value="1948">1948</option>
+ <option value="1949">1949</option>
+ <option value="1950">1950</option>
+ <option value="1951">1951</option>
+ <option value="1952">1952</option>
+ <option value="1953">1953</option>
+ <option value="1954">1954</option>
+ <option value="1955">1955</option>
+ <option value="1956">1956</option>
+ <option value="1957">1957</option>
+ <option value="1958">1958</option>
+ <option value="1959">1959</option>
+ <option value="1960">1960</option>
+ <option value="1961">1961</option>
+ <option value="1962">1962</option>
+ <option value="1963">1963</option>
+ <option value="1964">1964</option>
+ <option value="1965">1965</option>
+ <option value="1966">1966</option>
+ <option value="1967">1967</option>
+ <option value="1968">1968</option>
+ <option value="1969">1969</option>
+ <option value="1970">1970</option>
+ <option value="1971">1971</option>
+ <option value="1972">1972</option>
+ <option value="1973">1973</option>
+ <option value="1974">1974</option>
+ <option value="1975">1975</option>
+ <option value="1976">1976</option>
+ <option value="1977">1977</option>
+ <option value="1978">1978</option>
+ <option value="1979">1979</option>
+ <option value="1980">1980</option>
+ <option value="1981">1981</option>
+ <option value="1982">1982</option>
+ <option value="1983">1983</option>
+ <option value="1984">1984</option>
+ <option value="1985">1985</option>
+ <option value="1986">1986</option>
+ <option value="1987">1987</option>
+ <option value="1988">1988</option>
+ <option value="1989">1989</option>
+ <option value="1990">1990</option>
+ <option value="1991">1991</option>
+ <option value="1992">1992</option>
+ <option value="1993">1993</option>
+ <option value="1994">1994</option>
+ <option value="1995">1995</option>
+ <option value="1996">1996</option>
+ <option value="1997">1997</option>
+ <option value="1998">1998</option>
+ <option value="1999">1999</option>
+ <option value="2000">2000</option>
+ <option value="2001">2001</option>
+ <option value="2002">2002</option>
+ <option value="2003">2003</option>
+ <option value="2004">2004</option>
+ <option value="2005">2005</option>
+ <option value="2006">2006</option>
+ <option value="2007">2007</option>
+ <option value="2008">2008</option>
+ <option value="2009">2009</option>
+ <option value="2010">2010</option>
+ <option value="2011">2011</option>
+ <option value="2012">2012</option>
+ <option value="2013">2013</option>
+ <option value="2014">2014</option>
+ <option value="2015">2015</option>
+ <option value="2016">2016</option>
+ <option value="2017">2017</option>
+ </select>
+ </fieldset>
+ <div id="monetate_selectorHTML_b234e9d_0">
+ <div>
+ <label for="cbBillMeLaterElectronicConsent" id="lblBillMeLaterElectronicConsent">&nbsp;&nbsp;<input id="cbBillMeLaterElectronicConsent" name="bmlAcceptTerms" type="checkbox" />
+ </label>
+ </div>
+ </div>
+ </div>
+ <div id="BMLScroll">
+ <input name="bmlAcceptTermsTemp" id="cbBillMeLaterElectronicConsentTemp" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Note: After scrolling, please remain at the bottom of the Terms and Conditions section to continue. parseable name: bmlAcceptTermsTemp field signature: 1379157860 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="cbBillMeLaterElectronicConsent" id="lblBillMeLaterElectronicConsent">&nbsp;&nbsp;<input name="bmlAcceptTerms" id="cbBillMeLaterElectronicConsent" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Note: After scrolling, please remain at the bottom of the Terms and Conditions section to continue. parseable name: bmlAcceptTerms field signature: 4275106371 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </label>
+ </div>
+ </td>
+ </tr>
+ <tr id="trEnterNewCard-1">
+ <td colspan="5">
+ <input type="radio" name="rbNewPaymentMethod" id="rbNewCard" value="rbNewCard" checked="checked"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Enter New Card parseable name: rbNewPaymentMethod field signature: 95492298 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rbNewCard">&nbsp;Enter New Card</label>&nbsp;&nbsp;&nbsp;&nbsp;
+ </td>
+ </tr>
+ <tr id="trEnterNewCard-2">
+ <td colspan="5">
+ <div id="divEnterNewCard">
+ <fieldset>
+ <label id="lblNewCardType" for="selNewCardType">Type</label>
+ <select name="NewCardType" id="selNewCardType" size="1"
+title="overall type: CREDIT_CARD_TYPE server type: CREDIT_CARD_TYPE heuristic type: CREDIT_CARD_TYPE label: Type parseable name: NewCardType field signature: 3035337803 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_TYPE"
+>
+ <option value="AX">American Express</option>
+ <option value="CB">Carte Blanc</option>
+ <option value="DC">Diners Club</option>
+ <option value="SI">Discover</option>
+ <option value="MC">MasterCard</option>
+ <option value="QV">QCard</option>
+ <option value="VI" selected="selected">Visa</option>
+ </select>
+ <label id="lblNewCardNumber" for="txtNewCardNumber">Number:</label>
+ <input id="txtNewCardNumber" name="NewCardNumber" type="text" maxlength="20" size="21" autocomplete="off" autocorrect="off" value="" pattern="[0-9]*"
+title="overall type: CREDIT_CARD_NUMBER server type: CREDIT_CARD_NUMBER heuristic type: CREDIT_CARD_NUMBER label: Number: parseable name: NewCardNumber field signature: 2370218454 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_NUMBER"
+/>
+ <input id="hidNewLastCC" name="hidNewLastCC" type="hidden" value="" />
+ <fieldset id="fldExpireDateNewCard">
+ <label id="lblNewCard" for="selNewCard">Expiration Date:</label>
+ <select name="selNewCard" id="selNewCard" size="1"
+title="overall type: CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR server type: NO_SERVER_DATA heuristic type: CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR label: Expiration Date: parseable name: selNewCard field signature: 2308816317 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR"
+>
+ <option value="03/2017" selected="selected">03/2017</option>
+ <option value="04/2017">04/2017</option>
+ <option value="05/2017">05/2017</option>
+ <option value="06/2017">06/2017</option>
+ <option value="07/2017">07/2017</option>
+ <option value="08/2017">08/2017</option>
+ <option value="09/2017">09/2017</option>
+ <option value="10/2017">10/2017</option>
+ <option value="11/2017">11/2017</option>
+ <option value="12/2017">12/2017</option>
+ <option value="01/2018">01/2018</option>
+ <option value="02/2018">02/2018</option>
+ <option value="03/2018">03/2018</option>
+ <option value="04/2018">04/2018</option>
+ <option value="05/2018">05/2018</option>
+ <option value="06/2018">06/2018</option>
+ <option value="07/2018">07/2018</option>
+ <option value="08/2018">08/2018</option>
+ <option value="09/2018">09/2018</option>
+ <option value="10/2018">10/2018</option>
+ <option value="11/2018">11/2018</option>
+ <option value="12/2018">12/2018</option>
+ <option value="01/2019">01/2019</option>
+ <option value="02/2019">02/2019</option>
+ <option value="03/2019">03/2019</option>
+ <option value="04/2019">04/2019</option>
+ <option value="05/2019">05/2019</option>
+ <option value="06/2019">06/2019</option>
+ <option value="07/2019">07/2019</option>
+ <option value="08/2019">08/2019</option>
+ <option value="09/2019">09/2019</option>
+ <option value="10/2019">10/2019</option>
+ <option value="11/2019">11/2019</option>
+ <option value="12/2019">12/2019</option>
+ <option value="01/2020">01/2020</option>
+ <option value="02/2020">02/2020</option>
+ <option value="03/2020">03/2020</option>
+ <option value="04/2020">04/2020</option>
+ <option value="05/2020">05/2020</option>
+ <option value="06/2020">06/2020</option>
+ <option value="07/2020">07/2020</option>
+ <option value="08/2020">08/2020</option>
+ <option value="09/2020">09/2020</option>
+ <option value="10/2020">10/2020</option>
+ <option value="11/2020">11/2020</option>
+ <option value="12/2020">12/2020</option>
+ <option value="01/2021">01/2021</option>
+ <option value="02/2021">02/2021</option>
+ <option value="03/2021">03/2021</option>
+ <option value="04/2021">04/2021</option>
+ <option value="05/2021">05/2021</option>
+ <option value="06/2021">06/2021</option>
+ <option value="07/2021">07/2021</option>
+ <option value="08/2021">08/2021</option>
+ <option value="09/2021">09/2021</option>
+ <option value="10/2021">10/2021</option>
+ <option value="11/2021">11/2021</option>
+ <option value="12/2021">12/2021</option>
+ <option value="01/2022">01/2022</option>
+ <option value="02/2022">02/2022</option>
+ <option value="03/2022">03/2022</option>
+ <option value="04/2022">04/2022</option>
+ <option value="05/2022">05/2022</option>
+ <option value="06/2022">06/2022</option>
+ <option value="07/2022">07/2022</option>
+ <option value="08/2022">08/2022</option>
+ <option value="09/2022">09/2022</option>
+ <option value="10/2022">10/2022</option>
+ <option value="11/2022">11/2022</option>
+ <option value="12/2022">12/2022</option>
+ <option value="01/2023">01/2023</option>
+ <option value="02/2023">02/2023</option>
+ <option value="03/2023">03/2023</option>
+ <option value="04/2023">04/2023</option>
+ <option value="05/2023">05/2023</option>
+ <option value="06/2023">06/2023</option>
+ <option value="07/2023">07/2023</option>
+ <option value="08/2023">08/2023</option>
+ <option value="09/2023">09/2023</option>
+ <option value="10/2023">10/2023</option>
+ <option value="11/2023">11/2023</option>
+ <option value="12/2023">12/2023</option>
+ <option value="01/2024">01/2024</option>
+ <option value="02/2024">02/2024</option>
+ <option value="03/2024">03/2024</option>
+ <option value="04/2024">04/2024</option>
+ <option value="05/2024">05/2024</option>
+ <option value="06/2024">06/2024</option>
+ <option value="07/2024">07/2024</option>
+ <option value="08/2024">08/2024</option>
+ <option value="09/2024">09/2024</option>
+ <option value="10/2024">10/2024</option>
+ <option value="11/2024">11/2024</option>
+ <option value="12/2024">12/2024</option>
+ <option value="01/2025">01/2025</option>
+ <option value="02/2025">02/2025</option>
+ <option value="03/2025">03/2025</option>
+ <option value="04/2025">04/2025</option>
+ <option value="05/2025">05/2025</option>
+ <option value="06/2025">06/2025</option>
+ <option value="07/2025">07/2025</option>
+ <option value="08/2025">08/2025</option>
+ <option value="09/2025">09/2025</option>
+ <option value="10/2025">10/2025</option>
+ <option value="11/2025">11/2025</option>
+ <option value="12/2025">12/2025</option>
+ <option value="01/2026">01/2026</option>
+ <option value="02/2026">02/2026</option>
+ <option value="03/2026">03/2026</option>
+ <option value="04/2026">04/2026</option>
+ <option value="05/2026">05/2026</option>
+ <option value="06/2026">06/2026</option>
+ <option value="07/2026">07/2026</option>
+ <option value="08/2026">08/2026</option>
+ <option value="09/2026">09/2026</option>
+ <option value="10/2026">10/2026</option>
+ <option value="11/2026">11/2026</option>
+ <option value="12/2026">12/2026</option>
+ <option value="01/2027">01/2027</option>
+ <option value="02/2027">02/2027</option>
+ <option value="03/2027">03/2027</option>
+ <option value="04/2027">04/2027</option>
+ <option value="05/2027">05/2027</option>
+ <option value="06/2027">06/2027</option>
+ <option value="07/2027">07/2027</option>
+ <option value="08/2027">08/2027</option>
+ <option value="09/2027">09/2027</option>
+ <option value="10/2027">10/2027</option>
+ <option value="11/2027">11/2027</option>
+ <option value="12/2027">12/2027</option>
+ </select>
+ </fieldset>
+ </fieldset>
+ <fieldset id="fldSecurityCodeNewCard">
+ <div id="fldSecurityCode">
+ <label for="txtSecurityCode">Security Code:</label>&nbsp;<input id="txtSecurityCode" name="SecurityCode" type="text" pattern="[0-9]*" maxlength="5" size="5" value=""
+title="overall type: CREDIT_CARD_VERIFICATION_CODE server type: NO_SERVER_DATA heuristic type: CREDIT_CARD_VERIFICATION_CODE label: Security Code: parseable name: SecurityCode field signature: 4107652875 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+/>
+ </div>
+ </fieldset>
+ <div id="divQButton">
+ <span>
+ <input type="button" id="btnQCard" value="Add My QCard" />
+ </span>
+ <input type="hidden" id="addMyQCard" name="addMyQCard" value="false" />
+ <input type="hidden" id="isNPO" name="isNPO" value="true" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div id="divQvcGiftCardsMethod">
+ <div>
+ <div id="divGiftCardPaymentOption">
+ <input type="checkbox" name="cbGiftCard" id="cbGiftCard"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Use a Gift Card parseable name: cbGiftCard field signature: 2461714937 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="cbGiftCard">Use a Gift Card</label>
+ <div id="divQvcGiftCardEntry">
+ <fieldset>
+ <label for="txtQvcGiftCardNumber">Card Number:</label>
+ <input id="txtQvcGiftCardNumber" name="txtQvcGiftCardNumber" type="tel" autocomplete="off" autocorrect="off" maxlength="19" size="19" value=""
+title="overall type: CREDIT_CARD_NUMBER server type: CREDIT_CARD_NUMBER heuristic type: CREDIT_CARD_NUMBER label: Card Number: parseable name: txtQvcGiftCardNumber field signature: 375442765 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_NUMBER"
+/>
+ <label for="txtQvcGiftCardSecurityIdNumber">Security ID Number:</label>
+ <input id="txtQvcGiftCardSecurityIdNumber" name="txtQvcGiftCardSecurityIdNumber" type="text" autocomplete="off" autocorrect="off" maxlength="12" size="19" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Security ID Number: parseable name: txtQvcGiftCardSecurityIdNumber field signature: 383370886 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <span>
+ <input type="button" id="btnQvcGiftCardEnterAnotherCard" name="btnQvcGiftCardEnterAnotherCard" value="Enter Another Card" />
+ <span>Enter Another Card</span>
+ </span>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="monetate_selectorHTML_c937b2dd_0">
+ <div id="mainVoucherCodeDiv">
+ <input id="txtQvcApplyCodeNumber" name="txtQvcApplyCodeNumber" type="text" autocomplete="off" autocorrect="off" maxlength="25" size="19" value="" />
+ <input type="button" id="btnApplyCode" value="Apply Code" />
+ </div>
+ </div>
+ <div id="divButtons">
+ <span>
+ <input type="button" id="btnSubmitChanges" value="Continue Checkout" />
+ <span>Continue Checkout</span>
+ </span>
+ <span>
+ <span>Continue Checkout</span>
+ <input type="button" id="btnQCard2" value="Continue Checkout" />
+ </span>
+ <span>
+ <input type="button" id="btnReturnToOrder" value="EDIT SHOPPING CART" />
+ <span>Edit Shopping cart</span>
+ </span>
+ </div>
+ </form>
+ <form id="captureFormFooter" method="post" name="captureFormFooter">
+ <div id="divEmailFormFooter">
+ <label for="emailAddress1Footer">Get sneak previews of special offers and upcoming events delivered to your inbox.</label>
+ <span id="emailAddressErrorFooter">*</span>
+ <input id="emailAddress1Footer" type="text" value="Enter email" />
+ <input id="emailAddress2Footer" type="text" value="Confirm email" />
+ <input id="signUpFooter" type="submit" value="Sign Up" />
+ <span id="disclaimerTextFooter">*You're signing up to receive QVC promotional email.</span>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta property="fb:app_id" content="105047196722" />
+ <meta property="fb:page_id" content="23797290954" />
+ <meta name="format-detection" content="telephone=no" />
+ <meta name="viewport" content="width=919" />
+ <title>QVC.com Sign In</title>
+ </head>
+ <body>
+ <form id="frmMastheadSearch" method="get" action="http://www.qvc.com/CatalogSearch">
+ <fieldset>
+ <input type="hidden" name="langId" value="-1" />
+ <input type="hidden" name="storeId" value="10251" />
+ <input type="hidden" name="catalogId" value="10151" />
+ <label for="txtMastheadSearch">Search QVC:</label>
+ <input id="txtMastheadSearch" name="keyword" type="text" value="" autocomplete="off" autocorrect="off" placeholder="Search QVC" />
+ <input id="btnMastheadSearch" type="submit" alt="Go" value="Go" />
+ </fieldset>
+ </form>
+ <form id="frmSignIn" name="frmSignIn" method="post">
+ <input type="hidden" name="csrfToken" value="M1E1cTBwME9saUVZaU9iNUdJVnZqSm9JQThHM3gwSUt0elROSE9oSDJaST0=" />
+ <fieldset>
+ <input type="hidden" name="storeId" value="10251" />
+ <input type="hidden" name="catalogId" value="10151" />
+ <input type="hidden" name="langId" value="-1" />
+ <input type="hidden" name="URL" id="URL" value="http://www.qvc.com/Checkout?orderId=476661567&amp;langId=-1&amp;storeId=10251&amp;catalogId=10151" />
+ <input type="hidden" name="reLogonURL" value="LogonForm" />
+ <input type="hidden" name="rememberMe" id="rememberMe" value="true" />
+ <input type="hidden" name="fromPage" id="fromPage" value="checkout" />
+ <input type="hidden" name="orderId" value="476661567" />
+ </fieldset>
+ <div id="signInFields">
+ <label id="lblEmailAddress" for="txtEmailAddress">Email Address:</label>
+ <input id="txtEmailAddress" type="email" value="" maxlength="128" size="30" name="logonId" placeholder="Email Address" />
+ <div>
+ <label id="lblPassword" for="txtPassword">QVC Password:</label>
+ <input id="txtPassword" type="password" maxlength="24" size="30" name="logonPassword" placeholder="QVC Password" />
+ </div>
+ </div>
+ <div>
+ <div id="divUseDefaults">
+ <input id="cbUseDefaults" type="checkbox" name="cbReviewOrderTotal" value="on" />
+ <label id="lblUseDefaults" for="cbUseDefaults">Using your default shipping and payment information? Check the box to go directly to Order Summary.</label>
+ <input type="hidden" name="speedBuyTypeInd" id="speedBuyTypeInd" value="C" />
+ </div>
+ </div>
+ <div id="divFormButtons">
+ <div>
+ <span id="createPasswordSpan">
+ <span>Create Password</span>
+ <input id="btnSignIn" type="button" value="Create Password" />
+ </span>
+ <span id="continueButtonSpan">
+ <span>Continue</span>
+ <input id="btnSignIn" type="button" value="Continue" />
+ </span>
+ </div>
+ <div>
+ <span>
+ <span>Sign In</span>
+ <input id="btnSignIn" type="submit" value="Sign In" />
+ </span>
+ </div>
+ </div>
+ <fieldset>
+ <input id="userPrefs" type="hidden" value="" name="userPrefs" />
+ </fieldset>
+ </form>
+ <form id="frmCreateAccount">
+ <input type="hidden" name="csrfToken" value="M1E1cTBwME9saUVZaU9iNUdJVnZqSm9JQThHM3gwSUt0elROSE9oSDJaST0=" />
+ <div id="divButtons">
+ <span>
+ <span>Continue</span>
+ <input id="btnSignUp" type="button" value="Continue" />
+ </span>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta property="fb:app_id" content="105047196722" />
+ <meta property="fb:page_id" content="23797290954" />
+ <meta name="format-detection" content="telephone=no" />
+ <meta name="viewport" content="width=919" />
+ <title>Payment Method</title>
+ </head>
+ <body>
+ <form id="frmEditPaymentMethod" name="frmEditPaymentMethod" method="post" action="https://www.qvc.com/webapp/wcs/stores/servlet/NPOOrderAddPaymentMethods">
+ <input type="hidden" name="csrfToken" value="M1E1cTBwME9saUVZaU9iNUdJVnZqSm9JQThHM3gwSUt0elROSE9oSDJaST0=" />
+ <input type="hidden" name="ccId" value="" />
+ <input type="hidden" name="payMethodExpireMonth" value="" />
+ <input type="hidden" name="payMethodExpireYear" value="" />
+ <input type="hidden" name="ccDtime" value="" />
+ <input type="hidden" name="payMethodNewCard" value="N" />
+ <input type="hidden" name="payMethodExpiryEdited" value="N" />
+ <input type="hidden" name="payMethodCVV" value="" />
+ <input type="hidden" name="orderId" value="476661567" />
+ <input type="hidden" name="langId" value="-1" />
+ <input type="hidden" name="catalogId" value="10151" />
+ <input type="hidden" name="storeId" value="10251" />
+ <input type="hidden" name="dummydata" value="" />
+ <input type="hidden" id="currentPaymentMethod" name="currentPaymentMethod" value="VI" />
+ <input type="hidden" name="checkoutStep" value="3" />
+ <input type="hidden" name="fromPaymentPage" value="Y" />
+ <input type="hidden" name="URL" value="" />
+ <input type="hidden" id="addAnotherGC" name="addAnotherGC" value="N" />
+ <input type="hidden" name="BMLFilePath" id="BMLFilePath" value="https://www.qvc.com/wcsstore/US/content/html/popups/BillMeLaterTermsandConditions.html" />
+ <div id="divEasyPayOptions">
+ <fieldset>
+ <ul>
+ <li>
+ <input type="radio" value="Z" id="rb1PaymentsItem_1" name="rbItemEasyPayOption_1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: 4 Easy Pays of $39.74 parseable name: rbItemEasyPayOption_1 field signature: 915844214 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rb1PaymentsItem_1">4 Easy Pays of $39.74</label>
+ </li>
+ <li>
+ <input type="radio" value="N" id="rb2PaymentsItem_1" name="rbItemEasyPayOption_1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: 1 payment of $158.96 parseable name: rbItemEasyPayOption_1 field signature: 915844214 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rb2PaymentsItem_1">1 payment of $158.96</label>
+ </li>
+ </ul>
+ <input type="hidden" name="hItemEasyPayOptionPayMthd_1" id="hItemEasyPayOptionPayMthd_1" value="QVAXCBDCSIMCVIBL " />
+ <input type="hidden" name="orderItemId_1" id="orderItemId_1" value="660521668" />
+ </fieldset>
+ </div>
+ <div id="divNewPaymentMethod">
+ <input id="ccId_1" type="hidden" name="ccId_1" value="" />
+ <table id="tblNewPaymentMethod" border="0" cellspacing="0" cellpadding="0" width="100%" summary="Payment methods available for Checkout">
+ <tbody>
+ <tr id="trBillMeLaterRow-1">
+ <td>
+ <input type="radio" name="rbNewPaymentMethod" id="rbBillLater" value="rbBillLater"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: PayPal Credit parseable name: rbNewPaymentMethod field signature: 95492298 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rbBillLater">PayPal Credit</label>
+ </td>
+ </tr>
+ <tr id="trBillMeLaterRow-2">
+ <td colspan="5">
+ <div id="divBillMeLater">
+ <fieldset>
+ <label id="lblPrimaryPhone" for="txtPrimaryPhone">Home Phone:</label>
+ <input id="txtPrimaryPhone" type="tel" name="txtPrimaryPhone" autocomplete="off" autocorrect="off" value="" maxlength="14" size="14"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: Home Phone: parseable name: txtPrimaryPhone field signature: 918983855 form signature: 12190733459375771907"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+/>
+ </fieldset>
+ <fieldset>
+ <label id="lblEmailAddress" for="txtEmailAddress">Email Address:</label>
+ <input id="txtEmailAddress" type="email" name="txtEmailAddress" autocomplete="on" autocorrect="off" value=""
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: Email Address: parseable name: txtEmailAddress field signature: 653947670 form signature: 12190733459375771907"
+autofill-prediction="EMAIL_ADDRESS"
+/>
+ </fieldset>
+ <fieldset>
+ <label id="lblSsn" for="txtSsn">Social Security Number:</label>XXX-XX-<input id="txtSsn" type="text" pattern="[0-9]*" autocomplete="off" autocorrect="off" maxlength="4" size="4" name="txtLast4SSN" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Social Security Number: parseable name: txtLast4SSN field signature: 598258955 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </fieldset>
+ <fieldset>
+ <label id="lblDateOfBirthMonth" for="selDobMonth">Date of Birth:</label>
+ <select id="selDobMonth" name="dobMonth" size="1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Date of Birth: parseable name: dobMonth field signature: 3916402925 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="month" selected="selected">Month</option>
+ <option value="01">January</option>
+ <option value="02">February</option>
+ <option value="03">March</option>
+ <option value="04">April</option>
+ <option value="05">May</option>
+ <option value="06">June</option>
+ <option value="07">July</option>
+ <option value="08">August</option>
+ <option value="09">September</option>
+ <option value="10">October</option>
+ <option value="11">November</option>
+ <option value="12">December</option>
+ </select>
+ <label id="lblDateOfBirthDay" for="selDobDay">Day of Birth:</label>
+ <select id="selDobDay" name="dobDay" size="1"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Day of Birth: parseable name: dobDay field signature: 4127787517 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="day" selected="selected">Day</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ <option value="8">8</option>
+ <option value="9">9</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ <option value="24">24</option>
+ <option value="25">25</option>
+ <option value="26">26</option>
+ <option value="27">27</option>
+ <option value="28">28</option>
+ <option value="29">29</option>
+ <option value="30">30</option>
+ <option value="31">31</option>
+ </select>
+ <label id="lblDateOfBirthYear" for="selDobYear">Year of Birth:</label>
+ <select id="selDobYear" name="dobYear" size="1"
+title="overall type: COMPANY_NAME server type: COMPANY_NAME heuristic type: UNKNOWN_TYPE label: Year of Birth: parseable name: dobYear field signature: 3750696607 form signature: 12190733459375771907"
+autofill-prediction="COMPANY_NAME"
+>
+ <option value="year" selected="selected">Year</option>
+ <option value="1927">1927</option>
+ <option value="1928">1928</option>
+ <option value="1929">1929</option>
+ <option value="1930">1930</option>
+ <option value="1931">1931</option>
+ <option value="1932">1932</option>
+ <option value="1933">1933</option>
+ <option value="1934">1934</option>
+ <option value="1935">1935</option>
+ <option value="1936">1936</option>
+ <option value="1937">1937</option>
+ <option value="1938">1938</option>
+ <option value="1939">1939</option>
+ <option value="1940">1940</option>
+ <option value="1941">1941</option>
+ <option value="1942">1942</option>
+ <option value="1943">1943</option>
+ <option value="1944">1944</option>
+ <option value="1945">1945</option>
+ <option value="1946">1946</option>
+ <option value="1947">1947</option>
+ <option value="1948">1948</option>
+ <option value="1949">1949</option>
+ <option value="1950">1950</option>
+ <option value="1951">1951</option>
+ <option value="1952">1952</option>
+ <option value="1953">1953</option>
+ <option value="1954">1954</option>
+ <option value="1955">1955</option>
+ <option value="1956">1956</option>
+ <option value="1957">1957</option>
+ <option value="1958">1958</option>
+ <option value="1959">1959</option>
+ <option value="1960">1960</option>
+ <option value="1961">1961</option>
+ <option value="1962">1962</option>
+ <option value="1963">1963</option>
+ <option value="1964">1964</option>
+ <option value="1965">1965</option>
+ <option value="1966">1966</option>
+ <option value="1967">1967</option>
+ <option value="1968">1968</option>
+ <option value="1969">1969</option>
+ <option value="1970">1970</option>
+ <option value="1971">1971</option>
+ <option value="1972">1972</option>
+ <option value="1973">1973</option>
+ <option value="1974">1974</option>
+ <option value="1975">1975</option>
+ <option value="1976">1976</option>
+ <option value="1977">1977</option>
+ <option value="1978">1978</option>
+ <option value="1979">1979</option>
+ <option value="1980">1980</option>
+ <option value="1981">1981</option>
+ <option value="1982">1982</option>
+ <option value="1983">1983</option>
+ <option value="1984">1984</option>
+ <option value="1985">1985</option>
+ <option value="1986">1986</option>
+ <option value="1987">1987</option>
+ <option value="1988">1988</option>
+ <option value="1989">1989</option>
+ <option value="1990">1990</option>
+ <option value="1991">1991</option>
+ <option value="1992">1992</option>
+ <option value="1993">1993</option>
+ <option value="1994">1994</option>
+ <option value="1995">1995</option>
+ <option value="1996">1996</option>
+ <option value="1997">1997</option>
+ <option value="1998">1998</option>
+ <option value="1999">1999</option>
+ <option value="2000">2000</option>
+ <option value="2001">2001</option>
+ <option value="2002">2002</option>
+ <option value="2003">2003</option>
+ <option value="2004">2004</option>
+ <option value="2005">2005</option>
+ <option value="2006">2006</option>
+ <option value="2007">2007</option>
+ <option value="2008">2008</option>
+ <option value="2009">2009</option>
+ <option value="2010">2010</option>
+ <option value="2011">2011</option>
+ <option value="2012">2012</option>
+ <option value="2013">2013</option>
+ <option value="2014">2014</option>
+ <option value="2015">2015</option>
+ <option value="2016">2016</option>
+ <option value="2017">2017</option>
+ </select>
+ </fieldset>
+ <div id="monetate_selectorHTML_b234e9d_0">
+ <div>
+ <label for="cbBillMeLaterElectronicConsent" id="lblBillMeLaterElectronicConsent">&nbsp;&nbsp;<input id="cbBillMeLaterElectronicConsent" name="bmlAcceptTerms" type="checkbox" />
+ </label>
+ </div>
+ </div>
+ </div>
+ <div id="BMLScroll">
+ <input name="bmlAcceptTermsTemp" id="cbBillMeLaterElectronicConsentTemp" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Note: After scrolling, please remain at the bottom of the Terms and Conditions section to continue. parseable name: bmlAcceptTermsTemp field signature: 1379157860 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="cbBillMeLaterElectronicConsent" id="lblBillMeLaterElectronicConsent">&nbsp;&nbsp;<input name="bmlAcceptTerms" id="cbBillMeLaterElectronicConsent" type="checkbox"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Note: After scrolling, please remain at the bottom of the Terms and Conditions section to continue. parseable name: bmlAcceptTerms field signature: 4275106371 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </label>
+ </div>
+ </td>
+ </tr>
+ <tr id="trEnterNewCard-1">
+ <td colspan="5">
+ <input type="radio" name="rbNewPaymentMethod" id="rbNewCard" value="rbNewCard" checked="checked"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Enter New Card parseable name: rbNewPaymentMethod field signature: 95492298 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="rbNewCard">&nbsp;Enter New Card</label>&nbsp;&nbsp;&nbsp;&nbsp;
+ </td>
+ </tr>
+ <tr id="trEnterNewCard-2">
+ <td colspan="5">
+ <div id="divEnterNewCard">
+ <fieldset>
+ <label id="lblNewCardType" for="selNewCardType">Type</label>
+ <select name="NewCardType" id="selNewCardType" size="1"
+title="overall type: CREDIT_CARD_TYPE server type: CREDIT_CARD_TYPE heuristic type: CREDIT_CARD_TYPE label: Type parseable name: NewCardType field signature: 3035337803 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_TYPE"
+>
+ <option value="AX">American Express</option>
+ <option value="CB">Carte Blanc</option>
+ <option value="DC">Diners Club</option>
+ <option value="SI">Discover</option>
+ <option value="MC">MasterCard</option>
+ <option value="QV">QCard</option>
+ <option value="VI" selected="selected">Visa</option>
+ </select>
+ <label id="lblNewCardNumber" for="txtNewCardNumber">Number:</label>
+ <input id="txtNewCardNumber" name="NewCardNumber" type="text" maxlength="20" size="21" autocomplete="off" autocorrect="off" value="" pattern="[0-9]*"
+title="overall type: CREDIT_CARD_NUMBER server type: CREDIT_CARD_NUMBER heuristic type: CREDIT_CARD_NUMBER label: Number: parseable name: NewCardNumber field signature: 2370218454 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_NUMBER"
+/>
+ <input id="hidNewLastCC" name="hidNewLastCC" type="hidden" value="" />
+ <fieldset id="fldExpireDateNewCard">
+ <label id="lblNewCard" for="selNewCard">Expiration Date:</label>
+ <select name="selNewCard" id="selNewCard" size="1"
+title="overall type: CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR server type: NO_SERVER_DATA heuristic type: CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR label: Expiration Date: parseable name: selNewCard field signature: 2308816317 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR"
+>
+ <option value="03/2017" selected="selected">03/2017</option>
+ <option value="04/2017">04/2017</option>
+ <option value="05/2017">05/2017</option>
+ <option value="06/2017">06/2017</option>
+ <option value="07/2017">07/2017</option>
+ <option value="08/2017">08/2017</option>
+ <option value="09/2017">09/2017</option>
+ <option value="10/2017">10/2017</option>
+ <option value="11/2017">11/2017</option>
+ <option value="12/2017">12/2017</option>
+ <option value="01/2018">01/2018</option>
+ <option value="02/2018">02/2018</option>
+ <option value="03/2018">03/2018</option>
+ <option value="04/2018">04/2018</option>
+ <option value="05/2018">05/2018</option>
+ <option value="06/2018">06/2018</option>
+ <option value="07/2018">07/2018</option>
+ <option value="08/2018">08/2018</option>
+ <option value="09/2018">09/2018</option>
+ <option value="10/2018">10/2018</option>
+ <option value="11/2018">11/2018</option>
+ <option value="12/2018">12/2018</option>
+ <option value="01/2019">01/2019</option>
+ <option value="02/2019">02/2019</option>
+ <option value="03/2019">03/2019</option>
+ <option value="04/2019">04/2019</option>
+ <option value="05/2019">05/2019</option>
+ <option value="06/2019">06/2019</option>
+ <option value="07/2019">07/2019</option>
+ <option value="08/2019">08/2019</option>
+ <option value="09/2019">09/2019</option>
+ <option value="10/2019">10/2019</option>
+ <option value="11/2019">11/2019</option>
+ <option value="12/2019">12/2019</option>
+ <option value="01/2020">01/2020</option>
+ <option value="02/2020">02/2020</option>
+ <option value="03/2020">03/2020</option>
+ <option value="04/2020">04/2020</option>
+ <option value="05/2020">05/2020</option>
+ <option value="06/2020">06/2020</option>
+ <option value="07/2020">07/2020</option>
+ <option value="08/2020">08/2020</option>
+ <option value="09/2020">09/2020</option>
+ <option value="10/2020">10/2020</option>
+ <option value="11/2020">11/2020</option>
+ <option value="12/2020">12/2020</option>
+ <option value="01/2021">01/2021</option>
+ <option value="02/2021">02/2021</option>
+ <option value="03/2021">03/2021</option>
+ <option value="04/2021">04/2021</option>
+ <option value="05/2021">05/2021</option>
+ <option value="06/2021">06/2021</option>
+ <option value="07/2021">07/2021</option>
+ <option value="08/2021">08/2021</option>
+ <option value="09/2021">09/2021</option>
+ <option value="10/2021">10/2021</option>
+ <option value="11/2021">11/2021</option>
+ <option value="12/2021">12/2021</option>
+ <option value="01/2022">01/2022</option>
+ <option value="02/2022">02/2022</option>
+ <option value="03/2022">03/2022</option>
+ <option value="04/2022">04/2022</option>
+ <option value="05/2022">05/2022</option>
+ <option value="06/2022">06/2022</option>
+ <option value="07/2022">07/2022</option>
+ <option value="08/2022">08/2022</option>
+ <option value="09/2022">09/2022</option>
+ <option value="10/2022">10/2022</option>
+ <option value="11/2022">11/2022</option>
+ <option value="12/2022">12/2022</option>
+ <option value="01/2023">01/2023</option>
+ <option value="02/2023">02/2023</option>
+ <option value="03/2023">03/2023</option>
+ <option value="04/2023">04/2023</option>
+ <option value="05/2023">05/2023</option>
+ <option value="06/2023">06/2023</option>
+ <option value="07/2023">07/2023</option>
+ <option value="08/2023">08/2023</option>
+ <option value="09/2023">09/2023</option>
+ <option value="10/2023">10/2023</option>
+ <option value="11/2023">11/2023</option>
+ <option value="12/2023">12/2023</option>
+ <option value="01/2024">01/2024</option>
+ <option value="02/2024">02/2024</option>
+ <option value="03/2024">03/2024</option>
+ <option value="04/2024">04/2024</option>
+ <option value="05/2024">05/2024</option>
+ <option value="06/2024">06/2024</option>
+ <option value="07/2024">07/2024</option>
+ <option value="08/2024">08/2024</option>
+ <option value="09/2024">09/2024</option>
+ <option value="10/2024">10/2024</option>
+ <option value="11/2024">11/2024</option>
+ <option value="12/2024">12/2024</option>
+ <option value="01/2025">01/2025</option>
+ <option value="02/2025">02/2025</option>
+ <option value="03/2025">03/2025</option>
+ <option value="04/2025">04/2025</option>
+ <option value="05/2025">05/2025</option>
+ <option value="06/2025">06/2025</option>
+ <option value="07/2025">07/2025</option>
+ <option value="08/2025">08/2025</option>
+ <option value="09/2025">09/2025</option>
+ <option value="10/2025">10/2025</option>
+ <option value="11/2025">11/2025</option>
+ <option value="12/2025">12/2025</option>
+ <option value="01/2026">01/2026</option>
+ <option value="02/2026">02/2026</option>
+ <option value="03/2026">03/2026</option>
+ <option value="04/2026">04/2026</option>
+ <option value="05/2026">05/2026</option>
+ <option value="06/2026">06/2026</option>
+ <option value="07/2026">07/2026</option>
+ <option value="08/2026">08/2026</option>
+ <option value="09/2026">09/2026</option>
+ <option value="10/2026">10/2026</option>
+ <option value="11/2026">11/2026</option>
+ <option value="12/2026">12/2026</option>
+ <option value="01/2027">01/2027</option>
+ <option value="02/2027">02/2027</option>
+ <option value="03/2027">03/2027</option>
+ <option value="04/2027">04/2027</option>
+ <option value="05/2027">05/2027</option>
+ <option value="06/2027">06/2027</option>
+ <option value="07/2027">07/2027</option>
+ <option value="08/2027">08/2027</option>
+ <option value="09/2027">09/2027</option>
+ <option value="10/2027">10/2027</option>
+ <option value="11/2027">11/2027</option>
+ <option value="12/2027">12/2027</option>
+ </select>
+ </fieldset>
+ </fieldset>
+ <fieldset id="fldSecurityCodeNewCard">
+ <div id="fldSecurityCode">
+ <label for="txtSecurityCode">Security Code:</label>&nbsp;<input id="txtSecurityCode" name="SecurityCode" type="text" pattern="[0-9]*" maxlength="5" size="5" value=""
+title="overall type: CREDIT_CARD_VERIFICATION_CODE server type: NO_SERVER_DATA heuristic type: CREDIT_CARD_VERIFICATION_CODE label: Security Code: parseable name: SecurityCode field signature: 4107652875 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+/>
+ </div>
+ </fieldset>
+ <div id="divQButton">
+ <span>
+ <input type="button" id="btnQCard" value="Add My QCard" />
+ <span>Add My QCard</span>
+ </span>
+ <input type="hidden" id="addMyQCard" name="addMyQCard" value="false" />
+ <input type="hidden" id="isNPO" name="isNPO" value="true" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div id="divQvcGiftCardsMethod">
+ <div>
+ <div id="divGiftCardPaymentOption">
+ <input type="checkbox" name="cbGiftCard" id="cbGiftCard"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Use a Gift Card parseable name: cbGiftCard field signature: 2461714937 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="cbGiftCard">Use a Gift Card</label>
+ <div id="divQvcGiftCardEntry">
+ <fieldset>
+ <label for="txtQvcGiftCardNumber">Card Number:</label>
+ <input id="txtQvcGiftCardNumber" name="txtQvcGiftCardNumber" type="tel" autocomplete="off" autocorrect="off" maxlength="19" size="19" value=""
+title="overall type: CREDIT_CARD_NUMBER server type: CREDIT_CARD_NUMBER heuristic type: CREDIT_CARD_NUMBER label: Card Number: parseable name: txtQvcGiftCardNumber field signature: 375442765 form signature: 12190733459375771907"
+autofill-prediction="CREDIT_CARD_NUMBER"
+/>
+ <label for="txtQvcGiftCardSecurityIdNumber">Security ID Number:</label>
+ <input id="txtQvcGiftCardSecurityIdNumber" name="txtQvcGiftCardSecurityIdNumber" type="text" autocomplete="off" autocorrect="off" maxlength="12" size="19" value=""
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Security ID Number: parseable name: txtQvcGiftCardSecurityIdNumber field signature: 383370886 form signature: 12190733459375771907"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <span>
+ <input type="button" id="btnQvcGiftCardEnterAnotherCard" name="btnQvcGiftCardEnterAnotherCard" value="Enter Another Card" />
+ <span>Enter Another Card</span>
+ </span>
+ <br />
+ </fieldset>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="monetate_selectorHTML_c937b2dd_0">
+ <div id="mainVoucherCodeDiv">
+ <input id="txtQvcApplyCodeNumber" name="txtQvcApplyCodeNumber" type="text" autocomplete="off" autocorrect="off" maxlength="25" size="19" value="" />
+ <input type="button" id="btnApplyCode" value="Apply Code" />
+ </div>
+ </div>
+ <div id="divButtons">
+ <span>
+ <input type="button" id="btnSubmitChanges" value="Continue Checkout" />
+ <span>Continue Checkout</span>
+ </span>
+ <span>
+ <span>Continue Checkout</span>
+ <input type="button" id="btnQCard2" value="Continue Checkout" />
+ </span>
+ <span>
+ <input type="button" id="btnReturnToOrder" value="EDIT SHOPPING CART" />
+ <span>Edit Shopping cart</span>
+ </span>
+ </div>
+ </form>
+ <form id="captureFormFooter" method="post" name="captureFormFooter">
+ <div id="divEmailFormFooter">
+ <label for="emailAddress1Footer">Get sneak previews of special offers and upcoming events delivered to your inbox.</label>
+ <span id="emailAddressErrorFooter">*</span>
+ <input id="emailAddress1Footer" type="text" value="Enter email" />
+ <input id="emailAddress2Footer" type="text" value="Confirm email" />
+ <input id="signUpFooter" type="submit" value="Sign Up" />
+ <span id="disclaimerTextFooter">*You're signing up to receive QVC promotional email.</span>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>
+</title>
+ </head>
+ <body>
+ <meta http-equiv="imagetoolbar" content="no" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>Payment Options | Sears PartsDirect</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="decorator" content="pdwCheckout" />
+ <input type="hidden" id="isCQTOFPDPPagesEnabled" value="true" />
+ <input type="hidden" id="cqHost" value="//www.searspartsdirect.com" />
+ <form id="modelSearchHeader" method="get" action="https://www.searspartsdirect.com/partsdirect/getModel.pd" name="modelSearch">
+ <fieldset>
+ <label>Try searching again:</label>
+ <label for="searchedModelField">Model Number</label>
+ <input id="searchedModelField" type="text"
+title="Enter model number" value="Enter model number" name="modelNumberPopUp" maxlength="35" />
+ <input type="hidden" name="shdMod" />
+ <input type="hidden" name="pathTaken" />
+ <input type="hidden" name="legacySlrSearch" />
+ </fieldset>
+ </form>
+ <form id="creditCard" name="creditCard" action="https://www.searspartsdirect.com/partsdirect/checkOut.pd" method="post" pd-form-id="1489978179816">
+ <input type="hidden" name="_eventId" value="goAddPaymentOpt" id="creditCard__eventId" />
+ <input type="hidden" name="paymentOptionStr" value="" id="creditCard_paymentOptionStr" />
+ <input type="hidden" name="userPaymentTypeId" value="" id="creditCard_userPaymentTypeId" />
+ <input type="hidden" name="associateDiscountInput" value="" id="creditCard_associateDiscountInput" />
+ <input type="hidden" name="saveAssociateId" value="false" id="creditCard_saveAssociateId" />
+ <input type="hidden" name="userPaymentCommercial" value="false" id="creditCard_userPaymentCommercial" />
+ <input type="hidden" name="paymentType" value="" id="creditCard_paymentType" />
+ <div id="GuestCreditCardForm">
+ <div>
+ <div>
+ <label for="order.paymentType.cardNumber">Card Number</label>
+ <input type="text" name="maskedCardNumber" maxlength="16" value="" id="maskedCardNumber" placeholder="Card Number"
+title="overall type: CREDIT_CARD_NUMBER server type: CREDIT_CARD_NUMBER heuristic type: CREDIT_CARD_NUMBER label: Card Number parseable name: maskedCardNumber field signature: 2745159259 form signature: 7671147655436241539"
+autofill-prediction="CREDIT_CARD_NUMBER"
+/>
+ </div>
+ <div>
+ <label for="order.paymentType.nameOnCard">Name On Card</label>
+ <input type="text" name="creditCardPaymentName" value="" id="creditCard_creditCardPaymentName" placeholder="Name On Card"
+title="overall type: CREDIT_CARD_NAME_FULL server type: CREDIT_CARD_NAME_FULL heuristic type: CREDIT_CARD_NAME_FULL label: Name On Card parseable name: creditCardPaymentName field signature: 2311472685 form signature: 7671147655436241539"
+autofill-prediction="CREDIT_CARD_NAME_FULL"
+/>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="order.paymentType.securityCode">Security Code</label>
+ <input type="text" name="securityCode" value="" id="securityCode" placeholder="CVV"
+title="overall type: CREDIT_CARD_VERIFICATION_CODE server type: NO_SERVER_DATA heuristic type: CREDIT_CARD_VERIFICATION_CODE label: Security Code parseable name: securityCode field signature: 1305695504 form signature: 7671147655436241539"
+autofill-prediction="CREDIT_CARD_VERIFICATION_CODE"
+/>
+ </div>
+ <div>
+ <label for="order.paymentType.expirationDate">Expiration Date</label>
+ <select name="expMonth" id="expMonth"
+title="overall type: CREDIT_CARD_EXP_MONTH server type: CREDIT_CARD_EXP_MONTH heuristic type: CREDIT_CARD_EXP_MONTH label: Expiration Date parseable name: expMonth field signature: 2046285420 form signature: 7671147655436241539"
+autofill-prediction="CREDIT_CARD_EXP_MONTH"
+>
+ <option value="">Month</option>
+ <option value="1">01-January</option>
+ <option value="2">02-February</option>
+ <option value="3">03-March</option>
+ <option value="4">04-April</option>
+ <option value="5">05-May</option>
+ <option value="6">06-June</option>
+ <option value="7">07-July</option>
+ <option value="8">08-August</option>
+ <option value="9">09-September</option>
+ <option value="10">10-October</option>
+ <option value="11">11-November</option>
+ <option value="12">12-December</option>
+ </select>
+ </div>
+ <div>
+ <select name="expYear" id="expYear"
+title="overall type: CREDIT_CARD_EXP_4_DIGIT_YEAR server type: CREDIT_CARD_EXP_4_DIGIT_YEAR heuristic type: ADDRESS_HOME_CITY label: The City/State/ZIP Code combination you entered is incorrect. Please try again. Billing Address parseable name: expYear field signature: 2532266972 form signature: 7671147655436241539"
+autofill-prediction="CREDIT_CARD_EXP_4_DIGIT_YEAR"
+>
+ <option value="">Year</option>
+ <option value="2017">2017</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ <option value="2023">2023</option>
+ <option value="2024">2024</option>
+ <option value="2025">2025</option>
+ <option value="2026">2026</option>
+ <option value="2027">2027</option>
+ <option value="2028">2028</option>
+ <option value="2029">2029</option>
+ <option value="2030">2030</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ <form id="anotherBillingAddress" name="anotherBillingAddress" method="post" pd-form-id="1489978179817">
+ <div id="billingAddressForm">
+ <div>
+ <label for="order.billingInfo.firstName">First Name<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.firstName" maxlength="11" value="" id="order.billingInfo.firstName" placeholder="First Name *"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: First Name* parseable name: firstName field signature: 3077178767 form signature: 17982067175666068474"
+autofill-prediction="NAME_FIRST"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.lastName">Last Name<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.lastName" value="" id="order.billingInfo.lastName" placeholder="Last Name *"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: Last Name* parseable name: lastName field signature: 2325932944 form signature: 17982067175666068474"
+autofill-prediction="NAME_LAST"
+/>
+ </div>
+ <div id="divCityStateZipId_3">
+ <div id="divCityStateZipId_2">
+ <div>
+ <div>
+ <label for="order.billingInfo.address.address1">Street Address<span>*</span> - 24 character limit</label>
+ <input type="text" name="order.billingInfo.address.address1" value="Island Drvie" id="order.billingInfo.address.address1" placeholder="Street Address *"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: Street Address* - 24 character limit parseable name: address.address1 field signature: 796482076 form signature: 17982067175666068474"
+autofill-prediction="ADDRESS_HOME_LINE1"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.address.address2">Apt. #</label>
+ <input type="text" name="order.billingInfo.address.address2" value="" id="order.billingInfo.address.address2" placeholder="Apt. #"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Apt. # parseable name: address.address2 field signature: 1242999964 form signature: 17982067175666068474"
+autofill-prediction="ADDRESS_HOME_LINE2"
+/>
+ </div>
+ </div>
+ <div id="divCityStateZipId">
+ <div>
+ <div>
+ <label for="order.billingInfo.address.city">City<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.address.city" value="" id="order.billingInfo.address.city" placeholder="City *"
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: City* parseable name: address.city field signature: 1372321658 form signature: 17982067175666068474"
+autofill-prediction="ADDRESS_HOME_CITY"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.address.state">State<span>*</span>
+ </label>
+ <select name="order.billingInfo.address.state" id="order.billingInfo.address.state"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: State* parseable name: address.state field signature: 2106658457 form signature: 17982067175666068474"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="">ST *</option>
+ <option value="AA">AA</option>
+ <option value="AE">AE</option>
+ <option value="AL">AL</option>
+ <option value="AK">AK</option>
+ <option value="AP">AP</option>
+ <option value="AZ">AZ</option>
+ <option value="AR">AR</option>
+ <option value="CA">CA</option>
+ <option value="CO">CO</option>
+ <option value="CT">CT</option>
+ <option value="DE">DE</option>
+ <option value="DC">DC</option>
+ <option value="FL">FL</option>
+ <option value="GA">GA</option>
+ <option value="GU">GU</option>
+ <option value="HI">HI</option>
+ <option value="ID">ID</option>
+ <option value="IL">IL</option>
+ <option value="IN">IN</option>
+ <option value="IA">IA</option>
+ <option value="KS">KS</option>
+ <option value="KY">KY</option>
+ <option value="LA">LA</option>
+ <option value="ME">ME</option>
+ <option value="MD">MD</option>
+ <option value="MA">MA</option>
+ <option value="MI">MI</option>
+ <option value="MN">MN</option>
+ <option value="MS">MS</option>
+ <option value="MO">MO</option>
+ <option value="MT">MT</option>
+ <option value="NE">NE</option>
+ <option value="NV">NV</option>
+ <option value="NH">NH</option>
+ <option value="NJ">NJ</option>
+ <option value="NM">NM</option>
+ <option value="NY">NY</option>
+ <option value="NC">NC</option>
+ <option value="ND">ND</option>
+ <option value="OH">OH</option>
+ <option value="OK">OK</option>
+ <option value="OR">OR</option>
+ <option value="PA">PA</option>
+ <option value="PR">PR</option>
+ <option value="RI">RI</option>
+ <option value="SC">SC</option>
+ <option value="SD">SD</option>
+ <option value="TN">TN</option>
+ <option value="TX">TX</option>
+ <option value="UT">UT</option>
+ <option value="VA">VA</option>
+ <option value="VI">VI</option>
+ <option value="VT">VT</option>
+ <option value="WA">WA</option>
+ <option value="WV">WV</option>
+ <option value="WI">WI</option>
+ <option value="WY">WY</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label for="order.billingInfo.address.zipCode">ZIP/Postal Code<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.address.zipCode" value="" id="order.billingInfo.address.zipCode" placeholder="ZIP Code *"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: ZIP/Postal Code* parseable name: address.zipCode field signature: 1420459778 form signature: 17982067175666068474"
+autofill-prediction="ADDRESS_HOME_ZIP"
+/>
+ </div>
+ <input type="hidden" id="order.billingInfo.address.poBoxOrMillitaryAdd" name="order.billingInfo.address.poBoxOrMillitaryAdd" value="" />
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="order.billingInfo.dayTimePhone">Phone Number<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.dayTimePhone" value="" id="order.billingInfo.dayTimePhone" placeholder="Phone Number *"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: Phone Number* parseable name: dayTimePhone field signature: 2509269658 form signature: 17982067175666068474"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.dayTimePhoneExt">Ext.</label>
+ <input type="text" name="order.billingInfo.dayTimePhoneExt" value="" id="order.billingInfo.dayTimePhoneExt" placeholder="Ext."
+title="overall type: PHONE_HOME_CITY_CODE server type: PHONE_HOME_CITY_CODE heuristic type: PHONE_HOME_EXTENSION label: Ext. parseable name: dayTimePhoneExt field signature: 1836849076 form signature: 17982067175666068474"
+autofill-prediction="PHONE_HOME_CITY_CODE"
+/>
+ </div>
+ </div>
+ </div>
+ </form>
+ <form id="eCheck" name="eCheck" action="https://www.searspartsdirect.com/partsdirect/checkOut.pd" method="post" pd-form-id="1489978179818">
+ <input type="hidden" name="_eventId" value="goAddPaymentOpt" id="eCheck__eventId" />
+ <input type="hidden" name="paymentOptionStr" value="" id="eCheck_paymentOptionStr" />
+ <input type="hidden" name="userPaymentTypeId" value="" id="eCheck_userPaymentTypeId" />
+ <input type="hidden" name="associateDiscountInput" value="" id="eCheck_associateDiscountInput" />
+ <input type="hidden" name="saveAssociateId" value="false" id="eCheck_saveAssociateId" />
+ <input type="hidden" name="userPaymentCommercial" value="false" id="eCheck_userPaymentCommercial" />
+ <input type="hidden" name="paymentType" value="" id="eCheck_paymentType" />
+ <div>
+ <input type="radio" value="true" name="businessAccountFlag" id="fldBusinessAccountFlag"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: This is a Business Account parseable name: businessAccountFlag field signature: 1078565374 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="fldBusinessAccountFlag">This is a Business Account</label>
+ </div>
+ <div>
+ <input type="radio" value="false" checked="checked" name="businessAccountFlag" id="fldPersonalAccountFlag"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: This is a Personal Account parseable name: businessAccountFlag field signature: 1078565374 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <label for="fldPersonalAccountFlag">This is a Personal Account</label>
+ </div>
+ <label for="echeckFirstName">First name<span>*</span>
+ </label>
+ <div>
+ <input type="text" name="echeckFirstName" value="" id="echeckFirstName" placeholder="First name *"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: First name* parseable name: echeckFirstName field signature: 721631680 form signature: 11778620883203943321"
+autofill-prediction="NAME_FIRST"
+/>
+ </div>
+ <div>
+ <label for="echeckLastName">Last name<span>*</span>
+ </label>
+ <input type="text" name="echeckLastName" value="" id="echeckLastName" placeholder="Last name *"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: Last name* parseable name: echeckLastName field signature: 2195362343 form signature: 11778620883203943321"
+autofill-prediction="NAME_LAST"
+/>
+ </div>
+ <div>
+ <label for="maskedBankRoutingNumber">Bank routing number<span>*</span>
+ </label>
+ <input type="text" name="maskedBankRoutingNumber" value="" id="maskedBankRoutingNumber" placeholder="Bank routing number *"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Bank routing number* parseable name: maskedBankRoutingNumber field signature: 3997200887 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </div>
+ <div>
+ <label for="maskedCheckingAcctNumber">Checking account number<span>*</span>
+ </label>
+ <input type="text" name="maskedCheckingAcctNumber" value="" id="maskedCheckingAcctNumber" placeholder="Checking account number *"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Checking account number* parseable name: maskedCheckingAcctNumber field signature: 963371530 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </div>
+ <div>
+ <label for="checkNumber">Check number<span>*</span>
+ </label>
+ <input type="text" name="checkNumber" value="" id="checkNumber" placeholder="Check number *"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Check number* parseable name: checkNumber field signature: 1195469146 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </div>
+ <div id="personalAccountId">
+ <div>
+ <label for="maskedDriversLicence">Driver's license or state identification #<span>*</span>
+ </label>
+ <input type="text" name="maskedDriversLicence" value="" id="maskedDriversLicence" placeholder="Driver's license or state identification # *"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: Driver's license or state identification #* parseable name: maskedDriversLicence field signature: 1753257915 form signature: 11778620883203943321"
+autofill-prediction="ADDRESS_HOME_STATE"
+/>
+ </div>
+ <div>
+ <label for="state">State issued<span>*</span>
+ </label>
+ <select name="state" id="state"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: State issued* parseable name: state field signature: 1878375253 form signature: 11778620883203943321"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="">ST *</option>
+ <option value="AA">AA</option>
+ <option value="AE">AE</option>
+ <option value="AL">AL</option>
+ <option value="AK">AK</option>
+ <option value="AP">AP</option>
+ <option value="AZ">AZ</option>
+ <option value="AR">AR</option>
+ <option value="CA">CA</option>
+ <option value="CO">CO</option>
+ <option value="CT">CT</option>
+ <option value="DE">DE</option>
+ <option value="DC">DC</option>
+ <option value="FL">FL</option>
+ <option value="GA">GA</option>
+ <option value="GU">GU</option>
+ <option value="HI">HI</option>
+ <option value="ID">ID</option>
+ <option value="IL">IL</option>
+ <option value="IN">IN</option>
+ <option value="IA">IA</option>
+ <option value="KS">KS</option>
+ <option value="KY">KY</option>
+ <option value="LA">LA</option>
+ <option value="ME">ME</option>
+ <option value="MD">MD</option>
+ <option value="MA">MA</option>
+ <option value="MI">MI</option>
+ <option value="MN">MN</option>
+ <option value="MS">MS</option>
+ <option value="MO">MO</option>
+ <option value="MT">MT</option>
+ <option value="NE">NE</option>
+ <option value="NV">NV</option>
+ <option value="NH">NH</option>
+ <option value="NJ">NJ</option>
+ <option value="NM">NM</option>
+ <option value="NY">NY</option>
+ <option value="NC">NC</option>
+ <option value="ND">ND</option>
+ <option value="OH">OH</option>
+ <option value="OK">OK</option>
+ <option value="OR">OR</option>
+ <option value="PA">PA</option>
+ <option value="PR">PR</option>
+ <option value="RI">RI</option>
+ <option value="SC">SC</option>
+ <option value="SD">SD</option>
+ <option value="TN">TN</option>
+ <option value="TX">TX</option>
+ <option value="UT">UT</option>
+ <option value="VA">VA</option>
+ <option value="VI">VI</option>
+ <option value="VT">VT</option>
+ <option value="WA">WA</option>
+ <option value="WV">WV</option>
+ <option value="WI">WI</option>
+ <option value="WY">WY</option>
+ </select>
+ </div>
+ <div>
+ <label>Date of birth<span>*</span>
+ </label>
+ <select name="bdayMonth" id="bdayMonth"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Date of birth* parseable name: bdayMonth field signature: 1907288957 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="">Month</option>
+ <option value="1">January</option>
+ <option value="2">February</option>
+ <option value="3">March</option>
+ <option value="4">April</option>
+ <option value="5">May</option>
+ <option value="6">June</option>
+ <option value="7">July</option>
+ <option value="8">August</option>
+ <option value="9">September</option>
+ <option value="10">October</option>
+ <option value="11">November</option>
+ <option value="12">December</option>
+ </select>
+ <select name="bdayDate" id="bdayDate"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Date of birth* parseable name: bdayDate field signature: 2056433281 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="">Date</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ <option value="8">8</option>
+ <option value="9">9</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ <option value="24">24</option>
+ <option value="25">25</option>
+ <option value="26">26</option>
+ <option value="27">27</option>
+ <option value="28">28</option>
+ <option value="29">29</option>
+ <option value="30">30</option>
+ <option value="31">31</option>
+ </select>
+ <select name="bdayYear" id="bdayYear"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Date of birth* parseable name: bdayYear field signature: 938244373 form signature: 11778620883203943321"
+autofill-prediction="UNKNOWN_TYPE"
+>
+ <option value="">Year</option>
+ <option value="1999">1999</option>
+ <option value="1998">1998</option>
+ <option value="1997">1997</option>
+ <option value="1996">1996</option>
+ <option value="1995">1995</option>
+ <option value="1994">1994</option>
+ <option value="1993">1993</option>
+ <option value="1992">1992</option>
+ <option value="1991">1991</option>
+ <option value="1990">1990</option>
+ <option value="1989">1989</option>
+ <option value="1988">1988</option>
+ <option value="1987">1987</option>
+ <option value="1986">1986</option>
+ <option value="1985">1985</option>
+ <option value="1984">1984</option>
+ <option value="1983">1983</option>
+ <option value="1982">1982</option>
+ <option value="1981">1981</option>
+ <option value="1980">1980</option>
+ <option value="1979">1979</option>
+ <option value="1978">1978</option>
+ <option value="1977">1977</option>
+ <option value="1976">1976</option>
+ <option value="1975">1975</option>
+ <option value="1974">1974</option>
+ <option value="1973">1973</option>
+ <option value="1972">1972</option>
+ <option value="1971">1971</option>
+ <option value="1970">1970</option>
+ <option value="1969">1969</option>
+ <option value="1968">1968</option>
+ <option value="1967">1967</option>
+ <option value="1966">1966</option>
+ <option value="1965">1965</option>
+ <option value="1964">1964</option>
+ <option value="1963">1963</option>
+ <option value="1962">1962</option>
+ <option value="1961">1961</option>
+ <option value="1960">1960</option>
+ <option value="1959">1959</option>
+ <option value="1958">1958</option>
+ <option value="1957">1957</option>
+ <option value="1956">1956</option>
+ <option value="1955">1955</option>
+ <option value="1954">1954</option>
+ <option value="1953">1953</option>
+ <option value="1952">1952</option>
+ <option value="1951">1951</option>
+ <option value="1950">1950</option>
+ <option value="1949">1949</option>
+ <option value="1948">1948</option>
+ <option value="1947">1947</option>
+ <option value="1946">1946</option>
+ <option value="1945">1945</option>
+ <option value="1944">1944</option>
+ <option value="1943">1943</option>
+ <option value="1942">1942</option>
+ <option value="1941">1941</option>
+ <option value="1940">1940</option>
+ <option value="1939">1939</option>
+ <option value="1938">1938</option>
+ <option value="1937">1937</option>
+ <option value="1936">1936</option>
+ <option value="1935">1935</option>
+ <option value="1934">1934</option>
+ <option value="1933">1933</option>
+ <option value="1932">1932</option>
+ <option value="1931">1931</option>
+ <option value="1930">1930</option>
+ <option value="1929">1929</option>
+ <option value="1928">1928</option>
+ <option value="1927">1927</option>
+ <option value="1926">1926</option>
+ <option value="1925">1925</option>
+ <option value="1924">1924</option>
+ <option value="1923">1923</option>
+ <option value="1922">1922</option>
+ <option value="1921">1921</option>
+ <option value="1920">1920</option>
+ <option value="1919">1919</option>
+ <option value="1918">1918</option>
+ <option value="1917">1917</option>
+ <option value="1916">1916</option>
+ <option value="1915">1915</option>
+ <option value="1914">1914</option>
+ <option value="1913">1913</option>
+ <option value="1912">1912</option>
+ <option value="1911">1911</option>
+ <option value="1910">1910</option>
+ <option value="1909">1909</option>
+ <option value="1908">1908</option>
+ <option value="1907">1907</option>
+ <option value="1906">1906</option>
+ <option value="1905">1905</option>
+ <option value="1904">1904</option>
+ <option value="1903">1903</option>
+ <option value="1902">1902</option>
+ <option value="1901">1901</option>
+ <option value="1900">1900</option>
+ <option value="1899">1899</option>
+ </select>
+ </div>
+ </div>
+ </form>
+ <form id="emailForUpdates" name="emailForUpdates" action="https://www.searspartsdirect.com/partsdirect/offerEmailsAction.pd" method="post">
+ <fieldset>
+ <input type="text" id="emailAdd" name="emailAddress" value="" tabindex="4" maxlength="50" />
+ <label for="emailAdd">enter email address</label>
+ </fieldset>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>
+</title>
+ </head>
+ <body>
+ <meta http-equiv="imagetoolbar" content="no" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>Shipping address | Sears PartsDirect</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="decorator" content="pdwCheckout" />
+ <input type="hidden" id="isCQTOFPDPPagesEnabled" value="true" />
+ <input type="hidden" id="cqHost" value="//www.searspartsdirect.com" />
+ <form id="modelSearchHeader" method="get" action="https://www.searspartsdirect.com/partsdirect/getModel.pd" name="modelSearch" autocomplete="off">
+ <fieldset>
+ <label>Try searching again:</label>
+ <label for="searchedModelField">Model Number</label>
+ <input id="searchedModelField" type="text"
+title="Enter model number" value="Enter model number" name="modelNumberPopUp" maxlength="35" />
+ <input type="hidden" name="shdMod" />
+ <input type="hidden" name="pathTaken" />
+ <input type="hidden" name="legacySlrSearch" />
+ </fieldset>
+ </form>
+ <form id="forgotModalPswForm" name="forgotModalPswForm" action="https://www.searspartsdirect.com/partsdirect/initCheckoutAction.pd" method="post" autocomplete="off">
+ <input type="hidden" name="currentForgotPageURL" value="" id="currentForgotPageURL" />
+ <div>
+ <label for="email">Email</label>
+ </div>
+ <div>
+ <input type="text" name="email" id="forgotPwdFldEmail" />
+ </div>
+ <div>
+ <input type="hidden" value="true" name="isCaptchaEnabled" id="isCaptchaEnabled" />
+ <div>
+ <div>
+ <input type="hidden" value="VHM6MjAxNy0wMy0xOVQyMToxMjo1MFphYWExNjExNjM0OHp6ejIwMTctMDMtMTkgMjE6MTI6NTA=" name="captchaKey" id="forgotPwdFldCaptchaKey" />
+ <fieldset>
+ <input type="text" value="" tabindex="111" name="captchaText" id="forgotPwdFldCaptchaTxt" placeholder="Enter text" />
+ </fieldset>
+ </div>
+ </div>
+ <p>
+</p>
+ </div>
+ <input type="hidden" name="commercialUI" value="false" />
+ <input type="hidden" name="returnTo" value="#returnToVal" id="forgotModalPswForm_returnTo" />
+ <input type="hidden" name="commercialUI" value="#commercialUIVal" id="forgotModalPswForm_commercialUI" />
+ </form>
+ <form id="checkOut" name="checkOut" action="https://www.searspartsdirect.com/partsdirect/checkOut.pd" method="post" autocomplete="off" pd-form-id="1489975972490">
+ <input type="hidden" name="_eventId" value="goAddAddress" id="checkOut__eventId" />
+ <div id="shippingForm">
+ <input type="hidden" name="isShippingAddressChanged" value="false" id="isShippingAddressChanged" />
+ <input type="hidden" name="isShippingAddressEdited" value="false" id="isShippingAddressEdited" />
+ <div id="shippingFormContainer">
+ <div>
+ <label for="order.shippingInfo.firstName">First Name<span>*</span>
+ </label>
+ <input type="text" name="order.shippingInfo.firstName" maxlength="11" value="" id="checkOut_order_shippingInfo_firstName" placeholder="First Name *"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: First Name* parseable name: order.shippingInfo.firstName field signature: 243029182 form signature: 17155013134718564270"
+autofill-prediction="NAME_FIRST"
+/>
+ </div>
+ <div>
+ <label>Last Name<span>*</span>
+ </label>
+ <input type="text" name="order.shippingInfo.lastName" value="" id="checkOut_order_shippingInfo_lastName" placeholder="Last Name *"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: Last Name* parseable name: order.shippingInfo.lastName field signature: 1858327987 form signature: 17155013134718564270"
+autofill-prediction="NAME_LAST"
+/>
+ </div>
+ <div id="divCityStateZipId_3">
+ <div id="divCityStateZipId_2">
+ <div>
+ <div>
+ <label for="order.shippingInfo.address.originalAddress">Street Address<span>*</span> - 24 character limit</label>
+ <input type="text" name="order.shippingInfo.address.originalAddress" value="" id="checkOut_order_shippingInfo_address_originalAddress" placeholder="Street Address *"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: Street Address* - 24 character limit parseable name: order.shippingInfo.address.originalAddress field signature: 3461422700 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_LINE1"
+/>
+ <div>24 character limit</div>
+ </div>
+ <div>
+ <label for="order.shippingInfo.address.address2">Apt.
+ #</label>
+ <input type="text" name="order.shippingInfo.address.address2" value="" id="checkOut_order_shippingInfo_address_address2" placeholder="Apt. #"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Apt. # Apt. # parseable name: order.shippingInfo.address.address2 field signature: 1992141399 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_LINE2"
+/>
+ </div>
+ </div>
+ <div id="divCityStateZipId">
+ <div>
+ <div>
+ <label for="order.shippingInfo.address.city">City<span>*</span>
+ </label>
+ <input type="text" name="order.shippingInfo.address.city" value="" id="checkOut_order_shippingInfo_address_city" placeholder="City *"
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: City* parseable name: order.shippingInfo.address.city field signature: 2864354290 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_CITY"
+/>
+ </div>
+ <div>
+ <label for="order.shippingInfo.address.state">State<span>*</span>
+ </label>
+ <select name="order.shippingInfo.address.state" id="checkOut_order_shippingInfo_address_state"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: State* parseable name: order.shippingInfo.address.state field signature: 296886288 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="">ST *</option>
+ <option value="AA">AA</option>
+ <option value="AE">AE</option>
+ <option value="AL">AL</option>
+ <option value="AK">AK</option>
+ <option value="AP">AP</option>
+ <option value="AZ">AZ</option>
+ <option value="AR">AR</option>
+ <option value="CA">CA</option>
+ <option value="CO">CO</option>
+ <option value="CT">CT</option>
+ <option value="DE">DE</option>
+ <option value="DC">DC</option>
+ <option value="FL">FL</option>
+ <option value="GA">GA</option>
+ <option value="GU">GU</option>
+ <option value="HI">HI</option>
+ <option value="ID">ID</option>
+ <option value="IL">IL</option>
+ <option value="IN">IN</option>
+ <option value="IA">IA</option>
+ <option value="KS">KS</option>
+ <option value="KY">KY</option>
+ <option value="LA">LA</option>
+ <option value="ME">ME</option>
+ <option value="MD">MD</option>
+ <option value="MA">MA</option>
+ <option value="MI">MI</option>
+ <option value="MN">MN</option>
+ <option value="MS">MS</option>
+ <option value="MO">MO</option>
+ <option value="MT">MT</option>
+ <option value="NE">NE</option>
+ <option value="NV">NV</option>
+ <option value="NH">NH</option>
+ <option value="NJ">NJ</option>
+ <option value="NM">NM</option>
+ <option value="NY">NY</option>
+ <option value="NC">NC</option>
+ <option value="ND">ND</option>
+ <option value="OH">OH</option>
+ <option value="OK">OK</option>
+ <option value="OR">OR</option>
+ <option value="PA">PA</option>
+ <option value="PR">PR</option>
+ <option value="RI">RI</option>
+ <option value="SC">SC</option>
+ <option value="SD">SD</option>
+ <option value="TN">TN</option>
+ <option value="TX">TX</option>
+ <option value="UT">UT</option>
+ <option value="VA">VA</option>
+ <option value="VI">VI</option>
+ <option value="VT">VT</option>
+ <option value="WA">WA</option>
+ <option value="WV">WV</option>
+ <option value="WI">WI</option>
+ <option value="WY">WY</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label for="order.shippingInfo.address.zipCode">ZIP/Postal Code<span>*</span>
+ </label>
+ <input type="text" name="order.shippingInfo.address.zipCode" value="" id="checkOut_order_shippingInfo_address_zipCode" placeholder="ZIP Code *"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: ZIP/Postal Code* parseable name: order.shippingInfo.address.zipCode field signature: 2432211277 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_ZIP"
+/>
+ </div>
+ <input type="hidden" id="order.shippingInfo.address.poBoxOrMillitaryAdd" name="order.shippingInfo.address.poBoxOrMillitaryAdd" value="" />
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="order.shippingInfo.dayTimePhone">Phone Number<span>*</span>
+ </label>
+ <input type="text" name="order.shippingInfo.dayTimePhone" value="" id="checkOut_order_shippingInfo_dayTimePhone" placeholder="Phone Number *"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: Phone Number* parseable name: order.shippingInfo.dayTimePhone field signature: 3680252951 form signature: 17155013134718564270"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+/>
+ </div>
+ <div>
+ <label for="order.shippingInfo.dayTimePhoneExt">Ext.</label>
+ <input type="text" name="order.shippingInfo.dayTimePhoneExt" value="" id="checkOut_order_shippingInfo_dayTimePhoneExt" placeholder="Ext."
+title="overall type: PHONE_HOME_EXTENSION server type: NO_SERVER_DATA heuristic type: PHONE_HOME_EXTENSION label: Ext. parseable name: order.shippingInfo.dayTimePhoneExt field signature: 3095839543 form signature: 17155013134718564270"
+autofill-prediction="PHONE_HOME_EXTENSION"
+/>
+ </div>
+ </div>
+ <div>
+ <label for="order.shippingInfo.email">Email Address<span>*</span>
+ </label>
+ <input type="text" name="order.shippingInfo.email" value="" id="confirmEmailDiv" placeholder="Email Address *"
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: Email Address* parseable name: order.shippingInfo.email field signature: 928233428 form signature: 17155013134718564270"
+autofill-prediction="EMAIL_ADDRESS"
+/>
+ </div>
+ <div>
+ <label for="order.shippingInfo.emailConfirm">Confirm Email Address<span>*</span>
+ </label>
+ <input type="text" placeholder="Email Address *" name="order.shippingInfo.emailConfirm" value=""
+title="overall type: EMAIL_ADDRESS server type: EMAIL_ADDRESS heuristic type: EMAIL_ADDRESS label: Confirm Email Address* parseable name: order.shippingInfo.emailConfirm field signature: 3242992973 form signature: 17155013134718564270"
+autofill-prediction="EMAIL_ADDRESS"
+/>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="checkbox" name="order.shippingBillingSame" value="true" checked="checked" id="order.shippingBillingSame"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Same as shipping address parseable name: order.shippingBillingSame field signature: 1817157586 form signature: 17155013134718564270"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <input type="hidden" id="__checkbox_order.shippingBillingSame" name="__checkbox_order.shippingBillingSame" value="true" />
+ <label for="order.shippingBillingSame">Same as shipping address</label>
+ </div>
+ <div id="billingForm">
+ <div id="billingFormContainer">
+ <div id="billingAddressFormId">
+ <div>
+ <label for="order.billingInfo.firstName">First Name<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.firstName" maxlength="11" value="" id="checkOut_order_billingInfo_firstName" placeholder="First Name *"
+title="overall type: NAME_FIRST server type: NAME_FIRST heuristic type: NAME_FIRST label: First Name* parseable name: order.billingInfo.firstName field signature: 3077178767 form signature: 17155013134718564270"
+autofill-prediction="NAME_FIRST"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.lastName">Last Name<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.lastName" value="" id="checkOut_order_billingInfo_lastName" placeholder="Last Name *"
+title="overall type: NAME_LAST server type: NAME_LAST heuristic type: NAME_LAST label: Last Name* parseable name: order.billingInfo.lastName field signature: 2325932944 form signature: 17155013134718564270"
+autofill-prediction="NAME_LAST"
+/>
+ </div>
+ <div>
+ <div>
+ <label for="order.billingInfo.address.address1">Street Address<span>*</span> - 24 character limit</label>
+ <input type="text" name="order.billingInfo.address.address1" value="" id="checkOut_order_billingInfo_address_address1" placeholder="Street Address *"
+title="overall type: ADDRESS_HOME_LINE1 server type: ADDRESS_HOME_LINE1 heuristic type: ADDRESS_HOME_LINE1 label: Street Address* - 24 character limit parseable name: order.billingInfo.address.address1 field signature: 796482076 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_LINE1"
+/>
+ </div>
+ <div>
+ <label for="order.shippingInfo.address.address2">Apt.
+ #</label>
+ <input type="text" name="order.billingInfo.address.address2" value="" id="checkOut_order_billingInfo_address_address2" placeholder="Apt. #"
+title="overall type: ADDRESS_HOME_LINE2 server type: ADDRESS_HOME_LINE2 heuristic type: ADDRESS_HOME_LINE2 label: Apt. # parseable name: order.billingInfo.address.address2 field signature: 1242999964 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_LINE2"
+/>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label for="order.billingInfo.address.city">City<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.address.city" value="" id="checkOut_order_billingInfo_address_city" placeholder="City *"
+title="overall type: ADDRESS_HOME_CITY server type: ADDRESS_HOME_CITY heuristic type: ADDRESS_HOME_CITY label: City* parseable name: order.billingInfo.address.city field signature: 1372321658 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_CITY"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.address.state">State<span>*</span>
+ </label>
+ <select name="order.billingInfo.address.state" id="checkOut_order_billingInfo_address_state"
+title="overall type: ADDRESS_HOME_STATE server type: ADDRESS_HOME_STATE heuristic type: ADDRESS_HOME_STATE label: State* parseable name: order.billingInfo.address.state field signature: 2106658457 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_STATE"
+>
+ <option value="">ST *</option>
+ <option value="AA">AA</option>
+ <option value="AE">AE</option>
+ <option value="AL">AL</option>
+ <option value="AK">AK</option>
+ <option value="AP">AP</option>
+ <option value="AZ">AZ</option>
+ <option value="AR">AR</option>
+ <option value="CA">CA</option>
+ <option value="CO">CO</option>
+ <option value="CT">CT</option>
+ <option value="DE">DE</option>
+ <option value="DC">DC</option>
+ <option value="FL">FL</option>
+ <option value="GA">GA</option>
+ <option value="GU">GU</option>
+ <option value="HI">HI</option>
+ <option value="ID">ID</option>
+ <option value="IL">IL</option>
+ <option value="IN">IN</option>
+ <option value="IA">IA</option>
+ <option value="KS">KS</option>
+ <option value="KY">KY</option>
+ <option value="LA">LA</option>
+ <option value="ME">ME</option>
+ <option value="MD">MD</option>
+ <option value="MA">MA</option>
+ <option value="MI">MI</option>
+ <option value="MN">MN</option>
+ <option value="MS">MS</option>
+ <option value="MO">MO</option>
+ <option value="MT">MT</option>
+ <option value="NE">NE</option>
+ <option value="NV">NV</option>
+ <option value="NH">NH</option>
+ <option value="NJ">NJ</option>
+ <option value="NM">NM</option>
+ <option value="NY">NY</option>
+ <option value="NC">NC</option>
+ <option value="ND">ND</option>
+ <option value="OH">OH</option>
+ <option value="OK">OK</option>
+ <option value="OR">OR</option>
+ <option value="PA">PA</option>
+ <option value="PR">PR</option>
+ <option value="RI">RI</option>
+ <option value="SC">SC</option>
+ <option value="SD">SD</option>
+ <option value="TN">TN</option>
+ <option value="TX">TX</option>
+ <option value="UT">UT</option>
+ <option value="VA">VA</option>
+ <option value="VI">VI</option>
+ <option value="VT">VT</option>
+ <option value="WA">WA</option>
+ <option value="WV">WV</option>
+ <option value="WI">WI</option>
+ <option value="WY">WY</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label for="order.billingInfo.address.zipCode">ZIP/Postal Code<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.address.zipCode" value="" id="checkOut_order_billingInfo_address_zipCode" placeholder="ZIP Code *"
+title="overall type: ADDRESS_HOME_ZIP server type: ADDRESS_HOME_ZIP heuristic type: ADDRESS_HOME_ZIP label: ZIP/Postal Code* parseable name: order.billingInfo.address.zipCode field signature: 1420459778 form signature: 17155013134718564270"
+autofill-prediction="ADDRESS_HOME_ZIP"
+/>
+ </div>
+ <input type="hidden" id="order.shippingInfo.address.poBoxOrMillitaryAdd" name="order.shippingInfo.address.poBoxOrMillitaryAdd" value="" />
+ </div>
+ <div>
+ <div>
+ <label for="order.billingInfo.dayTimePhone">Phone Number<span>*</span>
+ </label>
+ <input type="text" name="order.billingInfo.dayTimePhone" value="" id="checkOut_order_billingInfo_dayTimePhone" placeholder="Phone Number *"
+title="overall type: PHONE_HOME_CITY_AND_NUMBER server type: PHONE_HOME_CITY_AND_NUMBER heuristic type: PHONE_HOME_WHOLE_NUMBER label: Phone Number* parseable name: order.billingInfo.dayTimePhone field signature: 2509269658 form signature: 17155013134718564270"
+autofill-prediction="PHONE_HOME_CITY_AND_NUMBER"
+/>
+ </div>
+ <div>
+ <label for="order.billingInfo.dayTimePhoneExt">Ext.</label>
+ <input type="text" name="order.billingInfo.dayTimePhoneExt" value="" id="checkOut_order_billingInfo_dayTimePhoneExt" placeholder="Ext."
+title="overall type: PHONE_HOME_CITY_CODE server type: PHONE_HOME_CITY_CODE heuristic type: PHONE_HOME_EXTENSION label: Ext. parseable name: order.billingInfo.dayTimePhoneExt field signature: 1836849076 form signature: 17155013134718564270"
+autofill-prediction="PHONE_HOME_CITY_CODE"
+/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="checkbox" name="orderSupport.saveShippingAddress" value="true" id="saveShipping"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Save my shipping address in My Profile. parseable name: orderSupport.saveShippingAddress field signature: 644246410 form signature: 17155013134718564270"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <input type="hidden" id="__checkbox_saveShipping" name="__checkbox_orderSupport.saveShippingAddress" value="true" />
+ <label for="saveShipping">Save my shipping address in My Profile.</label>
+ <input type="checkbox" name="orderSupport.saveBillingAddress" value="true" id="saveBilling"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Save my billing address in My Profile. parseable name: orderSupport.saveBillingAddress field signature: 2778625714 form signature: 17155013134718564270"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <input type="hidden" id="__checkbox_saveBilling" name="__checkbox_orderSupport.saveBillingAddress" value="true" />
+ <label for="saveBilling">Save my billing address in My Profile.</label>
+ </div>
+ </div>
+ <div id="createProfileContainer">
+ <div>
+ <label for="orderSupport.profilePassword">Password</label>
+ <input type="password" placeholder="Password" name="orderSupport.profilePassword"
+title="overall type: ACCOUNT_CREATION_PASSWORD server type: ACCOUNT_CREATION_PASSWORD heuristic type: UNKNOWN_TYPE label: Password Retype Password parseable name: orderSupport.profilePassword field signature: 2304557611 form signature: 17155013134718564270"
+autofill-prediction="ACCOUNT_CREATION_PASSWORD"
+/>
+ </div>
+ <div>
+ <label for="orderSupport.profilePassword">Retype Password</label>
+ <input type="password" placeholder="Retype Password" name="orderSupport.profilePasswordConfirm"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Retype Password parseable name: orderSupport.profilePasswordConfirm field signature: 196269180 form signature: 17155013134718564270"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ </div>
+ </div>
+ <div>
+ <input type="checkbox" name="order.shippingInfo.emailPromotion" value="true" checked="checked" id="order.shippingInfo.emailPromotion"
+title="overall type: UNKNOWN_TYPE server type: NO_SERVER_DATA heuristic type: UNKNOWN_TYPE label: Send me promotions, discounts and other special information from Sears.com. parseable name: order.shippingInfo.emailPromotion field signature: 2403849306 form signature: 17155013134718564270"
+autofill-prediction="UNKNOWN_TYPE"
+/>
+ <input type="hidden" id="__checkbox_order.shippingInfo.emailPromotion" name="__checkbox_order.shippingInfo.emailPromotion" value="true" />
+ <label for="order.shippingInfo.emailPromotion">Send me promotions, discounts and other special information from Sears.com.</label>
+ </div>
+ <div>
+ <ul>
+ <li>
+ <input type="submit"
+title="Continue checkout" value="Shipping Options" />
+ </li>
+ </ul>
+ </div>
+ </form>
+ <form id="verifyAddressForm" name="verifyAddressForm" action="https://www.searspartsdirect.com/partsdirect/checkOut.pd" method="post" autocomplete="off">
+ <input type="hidden" name="_eventId" value="goVerifyAddress" id="verifyAddressForm__eventId" />
+ <input type="hidden" id="CheckoutFlowAction_geoCode_submitted" name="geoCode" value="" />
+ </form>
+ <form id="emailForUpdates" name="emailForUpdates" action="https://www.searspartsdirect.com/partsdirect/offerEmailsAction.pd" method="post" autocomplete="off">
+ <fieldset>
+ <input type="text" id="emailAdd" name="emailAddress" value="" tabindex="4" maxlength="50" />
+ <label for="emailAdd">enter email address</label>
+ </fieldset>
+ </form>
+ <form id="shipSignForm" method="post" action="https://sso.shld.net/shccas/shcLogin" autocomplete="off" name="shipSignForm">
+ <input type="hidden" name="s" id="s" />
+ <input type="hidden" name="k" id="k" />
+ <input type="hidden" name="renew" value="true" id="renew" />
+ <input type="hidden" value="21" name="sourceSiteId" id="sourceSiteId" />
+ <input type="hidden" name="service" value="https://www.searspartsdirect.com/partsdirect/j_spring_cas_security_check" id="service" />
+ <div>
+ <label for="loginId">Email Address
+ <span>Required</span>
+ </label>
+ <input placeholder="Email Address" type="text" name="loginId" value="" id="email" />
+ </div>
+ <div>
+ <label for="password">Password<a tabindex="3">Forgot your password?</a>
+ </label>
+ <input placeholder="Password" type="password" value="" z-index="2000" name="logonPassword" id="password" />
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <title>It's easy to find the Office Supplies, Copy Paper,
+ Furniture, Ink, Toner, Cleaning Products, Electronics and the
+ Technology you need | Staples®</title>
+ <meta content="checkout" name="PageName">
+ <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
+ <meta content="noindex,follow" name="robots">
+ <meta content="Shop Staples® for everyday low prices and get everything you need for a home office or business. Staples Rewards� members get free shipping every day and up to 5% back in rewards, some exclusions apply."
+ name="description">
+ <meta name="viewport" content="width=null, initial-scale=1">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="x-dns-prefetch-control" content="on">
+ <meta http-equiv="content-language" content="en-us">
+</head>
+<body>
+ <form>
+ <input autocomplete="false" name="hidden" type="text">
+ <div>
+ <div>
+ <div>
+ <label for="firstName">
+<span>First Name</span>
+ <span>*</span>
+</label>
+<br>
+ <input name="firstName" maxlength="40" autocomplete="off"
+ placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="lastName">
+<span>Last
+ Name</span>
+<span>*</span>
+</label>
+<br>
+ <input name="lastName" maxlength="40" autocomplete="off"
+ placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="address1">Shipping
+ Address<span>*</span>
+</label>
+<br>
+ <input name="address1" maxlength="35" autocomplete="off" placeholder="" id="oneAutoComplete" type="text">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="emailId">
+<span>Email
+ Address</span>
+<span>*</span>
+</label>
+<br>
+ <input name="emailId" maxlength="80" autocomplete="off"
+ placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="phoneNo">
+<span>Phone</span>
+<span>*</span>
+</label>
+<br>
+ <input name="phoneNo" autocomplete="off" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="phoneEx">
+<span>Extn.</span>
+</label>
+<br>
+ <input name="phoneEx" maxlength="6" autocomplete="off"
+ placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="companyName">
+<span>Company Name
+ (optional)</span>
+</label>
+<br>
+ <input name="companyName" maxlength="50" autocomplete="off" placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <div tabindex="0">
+ <label>Email me exclusive offers & deals from
+ Staples</label>
+<input value="true" name="isEmailOfferChecked" type="hidden">
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <button type="submit">continue</button>
+ </div>
+ </div>
+ </div>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <title>It's easy to find the Office Supplies, Copy Paper,
+ Furniture, Ink, Toner, Cleaning Products, Electronics and the
+ Technology you need | Staples®</title>
+ <meta content="checkout" name="PageName">
+ <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
+ <meta content="noindex,follow" name="robots">
+ <meta content="Shop Staples® for everyday low prices and get everything you need for a home office or business. Staples Rewards� members get free shipping every day and up to 5% back in rewards, some exclusions apply."
+ name="description">
+ <meta name="viewport" content="width=null, initial-scale=1">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="x-dns-prefetch-control" content="on">
+ <meta http-equiv="content-language" content="en-us">
+</head>
+<body>
+ <form>
+ <input name="hidden" type="text">
+ <div>
+ <div>
+ <div>
+ <label for="firstName">
+<span>First Name</span>
+ <span>*</span>
+</label>
+<br>
+ <input name="firstName" maxlength="40" placeholder=""
+ type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="lastName">
+<span>Last
+ Name</span>
+<span>*</span>
+</label>
+<br>
+ <input name="lastName" maxlength="40" placeholder=""
+ type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="address1">Shipping
+ Address<span>*</span>
+</label>
+<br>
+ <input name="address1" maxlength="35" placeholder=""
+ id="oneAutoComplete" type="text">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="emailId">
+<span>Email
+ Address</span>
+<span>*</span>
+</label>
+<br>
+ <input name="emailId" maxlength="80" placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="phoneNo">
+<span>Phone</span>
+<span>*</span>
+</label>
+<br>
+ <input name="phoneNo" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="phoneEx">
+<span>Extn.</span>
+</label>
+<br>
+ <input name="phoneEx" maxlength="6" placeholder="" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="companyName">
+<span>Company Name
+ (optional)</span>
+</label>
+<br>
+ <input name="companyName" maxlength="50" placeholder=""
+ type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <div tabindex="0">
+ <label>Email me exclusive offers & deals from
+ Staples</label>
+<input value="true" name="isEmailOfferChecked" type="hidden">
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <button type="submit">continue</button>
+ </div>
+ </div>
+ </div>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <title>It's easy to find the Office Supplies, Copy Paper,
+ Furniture, Ink, Toner, Cleaning Products, Electronics and the
+ Technology you need | Staples®</title>
+ <meta content="checkout" name="PageName">
+ <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
+ <meta content="noindex,follow" name="robots">
+ <meta content="Shop Staples® for everyday low prices and get everything you need for a home office or business. Staples Rewards� members get free shipping every day and up to 5% back in rewards, some exclusions apply."
+ name="description">
+ <meta name="viewport" content="width=null, initial-scale=1">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="x-dns-prefetch-control" content="on">
+ <meta http-equiv="content-language" content="en-us">
+</head>
+<body>
+ <form id="payment-cc" name="payment-cc">
+ <input autocomplete="false" name="hidden" type="text">
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="cardNumber">
+<span>Card
+ Number</span>
+<span>*</span>
+</label>
+<br>
+ <input name="cardNumber" autocomplete="off" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="expDate">
+<span>Expiration
+ Date</span>
+<span>*</span>
+</label>
+<br>
+ <input name="expDate" placeholder="MM/YY" autocomplete="off" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="secCode">
+<span>Security
+ Code</span>
+<span>*</span>
+</label>
+<br>
+ <input name="secCode" value="" autocomplete="off"
+ maxlength="4" type="password">
+ </div>
+ </div>
+ </div>
+ <div>
+ <input value="" name="cardBrand" type="hidden">
+ </div>
+ <div>
+ <div>
+ <div tabindex="0">
+ <label>Billing address is the same as shipping
+ address</label>
+<input value="true" name="isBillAddrSameShipAddr" type="hidden">
+ </div>
+ </div>
+ </div>
+ <div>
+ <input value="1F16D1368309E3DBE15C391E7571B371" name="addressId" type="hidden">
+ </div>
+ <div>
+ <p>Purchase Order # (optional) <span tabindex="0">Add</span>
+</p>
+ </div>
+ <section id="paymentGuestRadio">
+</section>
+ <div>
+ <div>
+ <button type="submit">place my order</button>
+ </div>
+ </div>
+ <div>
+ <div>
+ <p>
+<span>By placing your order, you agree to
+ Staples</span>
+<a target="_blank">
+<span>Terms &
+ Conditions.</span>
+</a>
+</p>
+ </div>
+ </div>
+ </div>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <title>It's easy to find the Office Supplies, Copy Paper,
+ Furniture, Ink, Toner, Cleaning Products, Electronics and the
+ Technology you need | Staples®</title>
+ <meta content="checkout" name="PageName">
+ <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
+ <meta content="noindex,follow" name="robots">
+ <meta content="Shop Staples® for everyday low prices and get everything you need for a home office or business. Staples Rewards� members get free shipping every day and up to 5% back in rewards, some exclusions apply."
+ name="description">
+ <meta name="viewport" content="width=null, initial-scale=1">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta http-equiv="x-dns-prefetch-control" content="on">
+ <meta http-equiv="content-language" content="en-us">
+</head>
+<body>
+ <form id="payment-cc" name="payment-cc">
+ <input name="hidden" type="text">
+ <div>
+ <div>
+ <div>
+ <div>
+ <label for="cardNumber">
+<span>Card
+ Number</span>
+<span>*</span>
+</label>
+<br>
+ <input name="cardNumber" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="expDate">
+<span>Expiration
+ Date</span>
+<span>*</span>
+</label>
+<br>
+ <input name="expDate" placeholder="MM/YY" type="text">
+ </div>
+ </div>
+ <div>
+ <div>
+ <label for="secCode">
+<span>Security
+ Code</span>
+<span>*</span>
+</label>
+<br>
+ <input name="secCode" value="" maxlength="4" type="password">
+ </div>
+ </div>
+ </div>
+ <div>
+ <input value="" name="cardBrand" type="hidden">
+ </div>
+ <div>
+ <div>
+ <div tabindex="0">
+ <label>Billing address is the same as shipping
+ address</label>
+<input value="true" name="isBillAddrSameShipAddr" type="hidden">
+ </div>
+ </div>
+ </div>
+ <div>
+ <input value="1F16D1368309E3DBE15C391E7571B371" name="addressId" type="hidden">
+ </div>
+ <div>
+ <p>Purchase Order # (optional) <span tabindex="0">Add</span>
+</p>
+ </div>
+ <section id="paymentGuestRadio">
+</section>
+ <div>
+ <div>
+ <button type="submit">place my order</button>
+ </div>
+ </div>
+ <div>
+ <div>
+ <p>
+<span>By placing your order, you agree to
+ Staples</span>
+<a target="_blank">
+<span>Terms &
+ Conditions.</span>
+</a>
+</p>
+ </div>
+ </div>
+ </div>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <title>
+</title>
+ </head>
+ <body>
+ <form>
+ <p>Enter new zip code:</p>
+ <label>
+</label>
+ <div>
+ <p>
+<label>ZIP Code (required)</label>
+</p>
+ <label>
+<input name="zip-code" value="" placeholder="" type="text">
+</label>
+ </div>
+ <div>
+ <div>
+<button type="submit">Calculate</button>
+</div>
+ <div>
+<button type="button">Cancel</button>
+</div>
+ </div>
+ </form>
+ <form>
+ <label>
+<span>
+<span>Promo code
+ (optional)</span>
+</span>
+</label>
+ <div>
+<input name="promoCode">
+</div>
+ <button type="submit">
+<span>Apply</span>
+</button>
+ </form>
+ <form method="post" novalidate="">
+ <div>
+ <div>
+<label>
+<span>Email address (required)</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Email address" placeholder="" name="email" type="email">
+</label>
+</div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+<label>
+<span>Password (required)</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Password" placeholder="" name="password"
+ type="password">
+</label>
+</div>
+ <div>
+<label>
+<button type="button">
+<label>Show</label>
+</button>
+</label>
+</div>
+ </div>
+ <div>
+<label>
+<span>Password (required)</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Password" placeholder="" name="password"
+ autocomplete="off" tabindex="-1" type="text">
+</label>
+</div>
+ <div>
+<label>
+<button type="button" tabindex="-1">
+<label>Hide</label>
+</button>
+</label>
+</div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+<button type="button">Forgot password?</button>
+</div>
+ </div>
+ </div>
+ <div>
+<button type="submit">Sign In</button>
+</div>
+ </form>
+ <form novalidate="" method="post">
+ <div>*required field</div>
+ <div>
+<label>
+<span>First name*</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="First name" placeholder="" name="firstName" type="text">
+</label>
+</div>
+ </div>
+ <div>
+<label>
+<span>Last name*</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Last name" placeholder="" name="lastName"
+ type="text">
+</label>
+</div>
+ </div>
+ <div>
+<label>
+<span>Email address*</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Email address" placeholder="" name="email" autocomplete="off" type="email">
+</label>
+</div>
+ </div>
+ <div>
+<input tabindex="-1"
+title="Email Address" type="text">
+<input tabindex="-1" title="Password" type="password">
+</div>
+ <div>
+ <div>
+ <div>
+<label>
+<span>Password*</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Password" placeholder="" name="password"
+ autocomplete="new-password" type="password">
+<span>Your password
+ must be between 6 and 12 characters.</span>
+</label>
+ </div>
+ <div>
+<label>
+<button type="button">
+<label>Show</label>
+</button>
+</label>
+</div>
+ </div>
+ <div>
+<label>
+<span>Password*</span>
+</label>
+</div>
+ <div>
+ <div>
+<label>
+<input
+title="Password" placeholder="" name="password"
+ autocomplete="off" tabindex="-1" type="text">
+<span>Your password
+ must be between 6 and 12 characters.</span>
+</label>
+ </div>
+ <div>
+<label>
+<button type="button" tabindex="-1">
+<label>Hide</label>
+</button>
+</label>
+</div>
+ </div>
+ </div>
+ </div>
+ <div>
+<span>
+<span>By clicking Create Account, you acknowledge you
+ have read and agreed to our</span>
+<a target="_blank">Terms of
+ Use</a>
+<span>and</span>
+<a target="_blank">Privacy
+ Policy</a>
+<span>.</span>
+</span>
+ </div>
+ <button type="submit">Create Account</button>
+ <div>
+ <div>
+<input name="newsletter" id="checkbox-0" value="true" checked="checked" type="checkbox">
+<label for="checkbox-0">
+<span>Email me
+ about Rollbacks, special pricing, hot new items, gift ideas and
+ more.</span>
+</label>
+ </div>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <title>Checkout</title>
+ <meta property="og:type" content="Website">
+ <meta property="og:image" content="http://sphotos-b.xx.fbcdn.net/hphotos-ash4/229244_10150189115584236_162217_n.jpg">
+ <meta property="og:site_name" content="Walmart.com">
+ <meta property="fb:app_id" content="105223049547814">
+ <meta property="twitter:card" content="summary">
+ <meta property="twitter:image" content="https://pbs.twimg.com/profile_images/616833885/walmart_logo_youtube_bigger.jpg">
+ <meta property="twitter:site" content="@walmart">
+ <meta property="og:title" content="Checkout">
+ <meta property="twitter:title" content="Checkout">
+ </head>
+ <body>
+ <form>
+ <label>
+<span>
+<span>Promo code
+ (optional)</span>
+</span>
+</label>
+ <div>
+<input name="promoCode">
+</div>
+ <button type="submit">
+<span>Apply</span>
+</button>
+ </form>
+ <form>
+ <div>
+ <div>
+ <div>
+ <div>* required field</div>
+ <div>
+<label for="firstName">
+<span>First name on
+ card*</span>
+</label>
+ </div>
+ <div>
+ <div>
+<label for="firstName">
+<input id="firstName" value=""
+ name="firstName"
+title="First name" autocomplete="section-payment given-name" maxlength="25">
+</label>
+</div>
+ </div>
+ <div>
+<label for="lastName">
+<span>Last name on
+ card*</span>
+</label>
+ </div>
+ <div>
+ <div>
+<label for="lastName">
+<input id="lastName" value=""
+ name="lastName"
+title="Last name" autocomplete="section-payment family-name" maxlength="25">
+</label>
+</div>
+ </div>
+ <div>
+<label for="creditCard">
+<span>Card
+ number*</span>
+</label>
+ </div>
+ <div>
+ <div>
+<label for="creditCard">
+<input id="creditCard" pattern="[0-9]*" inputmode="numeric" name="creditCard" autocomplete="section-payment cc-number" maxlength="16">
+</label>
+</div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label>
+<span>Expiration date*</span>
+</label>
+ <div>
+ <span>
+ <label for="month-chooser">
+ <svg width="11" height="6">
+ <polygon fill="#027DC3" points="5.5,6 0,0 11,0">
+</polygon>
+ </svg>
+ <select id="month-chooser" name="month-chooser" autocomplete="section-payment cc-exp-month">
+ <option selected="selected" value="" disabled="disabled">
+ MM
+ </option>
+ <option value="01">01</option>
+ <option value="02">02</option>
+ <option value="03">03</option>
+ <option value="04">04</option>
+ <option value="05">05</option>
+ <option value="06">06</option>
+ <option value="07">07</option>
+ <option value="08">08</option>
+ <option value="09">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ </label>
+ </span>
+ </div>
+ <span>&nbsp;/&nbsp;</span>
+ <div>
+ <span>
+ <label for="year-chooser">
+ <svg width="11" height="6">
+ <polygon fill="#027DC3" points="5.5,6 0,0 11,0">
+</polygon>
+ </svg>
+ <select id="year-chooser" name="year-chooser" autocomplete="section-payment cc-exp-year">
+ <option selected="selected" value="" disabled="disabled">
+ YY
+ </option>
+ <option value="2017">17</option>
+ <option value="2018">18</option>
+ <option value="2019">19</option>
+ <option value="2020">20</option>
+ <option value="2021">21</option>
+ <option value="2022">22</option>
+ <option value="2023">23</option>
+ <option value="2024">24</option>
+ <option value="2025">25</option>
+ <option value="2026">26</option>
+ <option value="2027">27</option>
+ </select>
+ </label>
+ </span>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input
+title=" " name="brwsrAutofillText" type="text">
+ <input
+title=" " name="brwsrAutofillPassword" type="password">
+ <div>
+ <div>
+ <label for="cvv">
+<span>
+<span>
+<span>Security code*</span>
+ </span>
+</span>
+</label>
+ <div>
+<label for="cvv">
+<button type="button">
+<label for="cvv">
+<span name="help" size="1" alt="Icon for help">
+</span>
+</label>
+</button>
+</label>
+</div>
+ </div>
+ <div>
+ <div>
+<input id="cvv" name="cvv"
+title="cvv" value="" autocomplete="section-payment cc-csc" maxlength="3" pattern="[0-9]*" inputmode="numeric" type="password">
+</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+<label for="phone">
+<span>Phone number*</span>
+<span>Ex: (415)
+ 444 - 5555</span>
+</label>
+ </div>
+ <div>
+ <div>
+<label for="phone">
+<input id="phone" value="" name="phone"
+title="Phone" autocomplete="section-payment tel" maxlength="14" type="tel">
+</label>
+</div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+ <div>
+ <label>
+<input checked="checked" type="checkbox">
+</label>
+ <div>
+<label>
+<span>Same as shipping</span>
+</label>
+</div>
+ <div>
+ <p>
+<span>22F., No.55, Haiiu 1st Rd., Bafu Dist.,</span>
+<br>
+ <span>
+<span>San Bruno</span>
+<span>,</span>
+</span>
+ <span>
+<span>CA</span>
+</span>
+<span>94066</span>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div>
+<button type="button">
+<span>Review Your
+ Order</span>
+</button>
+ </div>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta name="generator" content="HTML Tidy for HTML5 for Mac OS X version 5.4.0">
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <title>Checkout</title>
+ <meta property="og:type" content="Website">
+ <meta property="og:image" content="http://sphotos-b.xx.fbcdn.net/hphotos-ash4/229244_10150189115584236_162217_n.jpg">
+ <meta property="og:site_name" content="Walmart.com">
+ <meta property="fb:app_id" content="105223049547814">
+ <meta property="twitter:card" content="summary">
+ <meta property="twitter:image" content="https://pbs.twimg.com/profile_images/616833885/walmart_logo_youtube_bigger.jpg">
+ <meta property="twitter:site" content="@walmart">
+ <meta property="og:title" content="Checkout">
+ <meta property="twitter:title" content="Checkout">
+ </head>
+ <body>
+ <form>
+ <p>Enter new zip code:</p>
+ <label>
+</label>
+ <div>
+ <p>
+<label>ZIP Code (required)</label>
+</p>
+ <label>
+<input name="zip-code" value="" placeholder="" type="text">
+</label>
+ </div>
+ <div>
+ <div>
+<button type="submit">Calculate</button>
+</div>
+ <div>
+<button type="button">Cancel</button>
+</div>
+ </div>
+ </form>
+ <form>
+ <label>
+<span>
+<span>Promo code
+ (optional)</span>
+</span>
+</label>
+ <div>
+<input name="promoCode">
+</div>
+ <button type="submit">
+<span>Apply</span>
+</button>
+ </form>
+ <form>
+ <div>
+ <div>
+ <div>
+ <div>
+ <div>*required field</div>
+ <label>
+<span>
+<span>
+<span>First name*</span>
+</span>
+</span>
+</label>
+ <div>
+<input
+title="First name" name="firstName" type="text">
+</div>
+ <label>
+<span>
+<span>
+<span>Last name*</span>
+</span>
+</span>
+</label>
+ <div>
+<input
+title="Last name" name="lastName" type="text">
+</div>
+ <label>
+<span>
+<span>
+<span>Phone number*</span>
+</span>
+</span>
+</label>
+ <div>
+<input minlength="10" maxlength="14"
+title="Phone number" name="phone" type="text">
+</div>
+ </div>
+ <div>
+ <label>
+<span>
+<span>
+<span>Street address*</span>
+</span>
+</span>
+</label>
+ <div>
+<input
+title="Street address" name="addressLineOne" type="text">
+</div>
+ <label>
+<span>
+<span>
+<span>Apt, suite, etc (optional)</span>
+</span>
+</span>
+</label>
+ <div>
+<input
+title="Apt, suite, bldg, c/o (optional)" name="addressLineTwo" type="text">
+</div>
+ <label>
+<span>
+<span>
+<span>City*</span>
+</span>
+</span>
+</label>
+ <div>
+<input
+title="City" name="city" value="" type="text">
+</div>
+ <div>
+ <div>
+ <div>
+ <label for="5">
+<span>State*</span>
+</label>
+ <div>
+ <div>
+ <select id="5"
+title="State" name="state">
+ <option value="">
+</option>
+ <option value="AL">Alabama</option>
+ <option value="AK">Alaska</option>
+ <option value="AZ">Arizona</option>
+ <option value="AR">Arkansas</option>
+ <option value="CA">California</option>
+ <option value="CO">Colorado</option>
+ <option value="CT">Connecticut</option>
+ <option value="DC">District of Columbia</option>
+ <option value="DE">Delaware</option>
+ <option value="FL">Florida</option>
+ <option value="GA">Georgia</option>
+ <option value="HI">Hawaii</option>
+ <option value="ID">Idaho</option>
+ <option value="IL">Illinois</option>
+ <option value="IN">Indiana</option>
+ <option value="IA">Iowa</option>
+ <option value="KS">Kansas</option>
+ <option value="KY">Kentucky</option>
+ <option value="LA">Louisiana</option>
+ <option value="ME">Maine</option>
+ <option value="MD">Maryland</option>
+ <option value="MA">Massachusetts</option>
+ <option value="MI">Michigan</option>
+ <option value="MN">Minnesota</option>
+ <option value="MS">Mississippi</option>
+ <option value="MO">Missouri</option>
+ <option value="MT">Montana</option>
+ <option value="NE">Nebraska</option>
+ <option value="NV">Nevada</option>
+ <option value="NH">New Hampshire</option>
+ <option value="NJ">New Jersey</option>
+ <option value="NM">New Mexico</option>
+ <option value="NY">New York</option>
+ <option value="NC">North Carolina</option>
+ <option value="ND">North Dakota</option>
+ <option value="OH">Ohio</option>
+ <option value="OK">Oklahoma</option>
+ <option value="OR">Oregon</option>
+ <option value="PA">Pennsylvania</option>
+ <option value="RI">Rhode Island</option>
+ <option value="SC">South Carolina</option>
+ <option value="SD">South Dakota</option>
+ <option value="TN">Tennessee</option>
+ <option value="TX">Texas</option>
+ <option value="UT">Utah</option>
+ <option value="VT">Vermont</option>
+ <option value="VA">Virginia</option>
+ <option value="WA">Washington</option>
+ <option value="WV">West Virginia</option>
+ <option value="WI">Wisconsin</option>
+ <option value="WY">Wyoming</option>
+ <option value="AA">Armed Forces Americas</option>
+ <option value="AP">Armed Forces Pacific</option>
+ <option value="AE">Armed Forces other</option>
+ <option value="AS">American Samoa</option>
+ <option value="GU">Guam</option>
+ <option value="MP">N. Mariana Islands</option>
+ <option value="PW">Palau</option>
+ <option value="PR">Puerto Rico</option>
+ <option value="VI">Virgin Islands</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <label>
+<span>
+<span>
+<span>ZIP
+ Code*</span>
+</span>
+</span>
+</label>
+ <div>
+<input
+title="Zip code" name="postalCode" value="" type="text">
+</div>
+ </div>
+ </div>
+ </div>
+ <div>
+<input id="4" name="isDefault" type="checkbox">
+<label for="4">
+<span>Set as my preferred address</span>
+</label>
+</div>
+ </div>
+ </div>
+ </div>
+ </form>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Address Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Address Demo Page (without autocomplete attribute)</h1>
+ <form id="form">
+ <p><label>givenname: <input type="text" id="given-name" name="given-name"/></label></p>
+ <p><label>familyname: <input type="text" id="family-name" name="family-name"/></label></p>
+ <p><label>organization: <input type="text" id="organization" name="organization"/></label></p>
+ <p><label>streetAddress: <input type="text" id="street-address" name="street-address"/></label></p>
+ <p><label>addressLevel2: <input type="text" id="address-level2" name="address-level2"/></label></p>
+ <p><label>addressLevel1: <input type="text" id="address-level1" name="address-level1"/></label></p>
+ <p><label>postalCode: <input type="text" id="postal-code" name="postal-code"/></label></p>
+ <p><label>country: <input type="text" id="country" name="country"/></label></p>
+ <p><label>tel: <input type="text" id="tel" name="tel"/></label></p>
+ <p><label>email: <input type="text" id="email" name="email"/></label></p>
+ <p>
+ <input type="submit"/>
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Form Autofill Credit Card Demo Page</title>
+</head>
+<body>
+ <h1>Form Autofill Credit Card Demo Page (without Autocomplete attribute)</h1>
+ <!-- All credit card fields are in the same form -->
+ <form id="form">
+ <p><label>Name: <input id="cc-name" placeholder="Credit Card Name"></label></p>
+ <p><label>Card Number: <input id="cc-number" placeholder="Credit Card Number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" placeholder="Credit Card Expiration Month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" placeholder="Credit Card Expiration Year"></label></p>
+ <p><label>CSC: <input id="cc-csc" placeholder="Credit Card Security Code"></label></p>
+ <p><label>Card Type: <select id="cc-type" placeholder="Credit Card Type">
+ <option></option>
+ <option value="discover">Discover</option>
+ <option value="jcb">JCB</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">MasterCard</option>
+ <option value="gringotts">Unknown card network</option>
+ </select></label></p>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+
+ <!-- cc-number in one form, other credit card fields in another form -->
+ <form id="form2-cc-number">
+ <p><label>Card Number: <input id="cc-number" placeholder="Credit Card Number"></label></p>
+ </form>
+ <form id="form2-cc-other">
+ <p><label>Name: <input id="cc-name" placeholder="Credit Card Name"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" placeholder="Credit Card Expiration Month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" placeholder="Credit Card Expiration Year"></label></p>
+ <p><label>CSC: <input id="cc-csc" placeholder="Credit Card Security Code"></label></p>
+ <p><label>Card Type: <select id="cc-type" placeholder="Credit Card Type">
+ <option></option>
+ <option value="discover">Discover</option>
+ <option value="jcb">JCB</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">MasterCard</option>
+ <option value="gringotts">Unknown card network</option>
+ </select></label></p>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../..//toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form credit card autofill
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const reducedMockRecord = {
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+};
+
+async function setupCreditCardStorage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+}
+
+async function setupFormHistory() {
+ await updateFormHistory([
+ {op: "add", fieldname: "cc-name", value: "John Smith"},
+ {op: "add", fieldname: "cc-exp-year", value: 2023},
+ ]);
+}
+
+initPopupListener();
+
+// Form with history only.
+add_task(async function history_only_menu_checking() {
+ // TODO: eliminate the timeout when we're able to indicate the right
+ // timing to start.
+ //
+ // After test process was re-spawning to https scheme. Wait 2 secs
+ // to ensure the environment is ready to do storage setup.
+ await sleep(2000);
+ await setupFormHistory();
+
+ await setInput("#cc-exp-year", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["2023"], false);
+});
+
+// Display credit card result even if the number of fillable fields is less than the threshold.
+add_task(async function all_saved_fields_less_than_threshold() {
+ await addCreditCard(reducedMockRecord);
+
+ await setInput("#cc-name", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries([reducedMockRecord].map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `Visa ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await cleanUpCreditCards();
+});
+
+// Form with both history and credit card storage.
+add_task(async function check_menu_when_both_existed() {
+ await setupCreditCardStorage();
+
+ await setInput("#cc-number", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primaryAffix: cc.ccNumberFmt.affix,
+ primary: cc.ccNumberFmt.label,
+ secondary: cc["cc-name"],
+ ariaLabel: `${getCCTypeName(cc)} ${cc.ccNumberFmt.affix} ${cc.ccNumberFmt.label} ${cc["cc-name"]}`,
+ })));
+
+ await setInput("#cc-name", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await setInput("#cc-exp-year", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-exp-year"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-year"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await setInput("#cc-exp-month", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-exp-month"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-month"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await cleanUpCreditCards();
+});
+
+// Display history search result if no matched data in credit card.
+add_task(async function check_fallback_for_mismatched_field() {
+ await addCreditCard(reducedMockRecord);
+
+ await setInput("#cc-exp-year", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["2023"], false);
+
+ await cleanUpCreditCards();
+});
+
+// Display history search result if credit card autofill is disabled.
+add_task(async function check_search_result_for_pref_off() {
+ await setupCreditCardStorage();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.formautofill.creditCards.enabled", false]],
+ });
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["John Smith"], false);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+let canTest;
+
+// Autofill the credit card from dropdown menu.
+add_task(async function check_fields_after_form_autofill() {
+ canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ await setInput("#cc-exp-year", 202);
+
+ synthesizeKey("KEY_ArrowDown");
+ // The popup doesn't auto-show on focus because the field isn't empty
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-exp-year"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-year"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ synthesizeKey("KEY_ArrowDown");
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+ await osKeyStoreLoginShown;
+});
+
+// Fallback to history search after autofill values (for non-empty fields).
+add_task(async function check_fallback_after_form_autofill() {
+ if (!canTest) {
+ return;
+ }
+
+ await setInput("#cc-name", "J", true);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["John Smith"], false);
+});
+
+// Present credit card popup immediately when user blanks a field
+add_task(async function check_cc_popup_on_field_blank() {
+ if (!canTest) {
+ return;
+ }
+
+ await setInput("#cc-name", "", true);
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+});
+
+// Resume form autofill once all the autofilled fileds are changed.
+add_task(async function check_form_autofill_resume() {
+ if (!canTest) {
+ return;
+ }
+
+ document.querySelector("#cc-name").blur();
+ document.querySelector("#form1").reset();
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+ <p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
+ <option value="discover">Discover</option>
+ <option value="jcb">JCB</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">MasterCard</option>
+ </select></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - clear form button</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: clear form button
+
+<script>
+"use strict";
+
+const MOCK_ADDR_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "2 Harrison St\nline2\nline3",
+ tel: "+13453453456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+}, {
+ organization: "Tel org",
+ tel: "+12223334444",
+}];
+const MOCK_CC_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const MOCK_CC_STORAGE_EXPECTED_FILL = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": "04",
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": "12",
+ "cc-exp-year": 2022,
+}];
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addAddress(MOCK_ADDR_STORAGE[0]);
+ await addAddress(MOCK_ADDR_STORAGE[1]);
+ await addAddress(MOCK_ADDR_STORAGE[2]);
+
+ await addCreditCard(MOCK_CC_STORAGE[0]);
+ await addCreditCard(MOCK_CC_STORAGE[1]);
+});
+
+
+async function checkIsFormCleared(patch = {}) {
+ const form = document.getElementById("form1");
+
+ for (const elem of form.elements) {
+ const expectedValue = patch[elem.id] || "";
+ checkFieldValue(elem, expectedValue);
+ await checkFieldHighlighted(elem, false);
+ await checkFieldPreview(elem, "");
+ }
+}
+
+async function confirmClear(selector) {
+ info("Await for clearing input");
+ let promise = new Promise(resolve => {
+ let beforeInputFired = false;
+ let element = document.querySelector(selector);
+ element.addEventListener("beforeinput", (event) => {
+ beforeInputFired = true;
+ ok(event instanceof InputEvent,
+ '"beforeinput" event should be dispatched with InputEvent interface');
+ is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
+ `"beforeinput" event should be cancelable unless it's disabled by the pref`);
+ is(event.bubbles, true,
+ '"beforeinput" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "beforeinput" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "beforeinput" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "beforeinput" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "beforeinput" event should return empty array');
+ }, {once: true});
+ element.addEventListener("input", (event) => {
+ ok(beforeInputFired, `"beforeinput" event should've been fired before "input" on <${element.tagName} type="${element.type}">`);
+ ok(event instanceof InputEvent,
+ '"input" event should be dispatched with InputEvent interface');
+ is(event.cancelable, false,
+ '"input" event should be never cancelable');
+ is(event.bubbles, true,
+ '"input" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "input" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "input" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "input" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "input" should return empty array');
+ resolve();
+ }, {once: true})
+ });
+ synthesizeKey("KEY_Enter");
+ await promise;
+}
+
+add_task(async function simple_clear() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await triggerPopupAndHoverItem("#tel", 0);
+ await confirmClear("#tel");
+ await checkIsFormCleared();
+});
+
+add_task(async function clear_adapted_record() {
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await confirmClear("#street-address");
+ await checkIsFormCleared();
+});
+
+add_task(async function clear_modified_form() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await setInput("#tel", "+1111111111", true);
+
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await confirmClear("#street-address");
+ await checkIsFormCleared({tel: "+1111111111"});
+});
+
+add_task(async function clear_distinct_section() {
+ if (!(await canTestOSKeyStoreLogin())) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ document.getElementById("form1").reset();
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE_EXPECTED_FILL[0]);
+ await osKeyStoreLoginShown;
+
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await confirmClear("#street-address");
+
+ for (const [id, val] of Object.entries(MOCK_CC_STORAGE_EXPECTED_FILL[0])) {
+ const element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+ checkFieldValue(element, val);
+ await checkFieldHighlighted(element, true);
+ }
+
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ await confirmClear("#cc-name");
+ await checkIsFormCleared();
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" autocomplete="organization"></label></p>
+ <p><label>streetAddress: <input id="street-address" autocomplete="street-address"></label></p>
+ <p><label>tel: <input id="tel" autocomplete="tel"></label></p>
+ <p><label>country: <input id="country" autocomplete="country"></label></p>
+
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - clear form button with select elements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: clear form button with select elements.
+
+<script>
+"use strict";
+const MOCK_ADDR_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "2 Harrison St\nline2\nline3",
+ tel: "+13453453456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+}, {
+ organization: "Tel org",
+ tel: "+12223334444",
+}];
+
+const MOCK_CC_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addAddress(MOCK_ADDR_STORAGE[0]);
+ await addCreditCard(MOCK_CC_STORAGE[0]);
+ await addCreditCard(MOCK_CC_STORAGE[1]);
+});
+
+
+async function checkIsFormCleared(patch = {}) {
+ const form = document.getElementById("form1");
+
+ for (const elem of form.elements) {
+ const expectedValue = patch[elem.id] || "";
+ checkFieldValue(elem, expectedValue);
+ await checkFieldHighlighted(elem, false);
+ await checkFieldPreview(elem, "");
+ }
+}
+
+async function confirmClear(selector) {
+ info("Await for clearing input");
+ let promise = new Promise(resolve => {
+ let beforeInputFired = false;
+ let element = document.querySelector(selector);
+ info(`Which element are we clearing? ${element.id}`);
+ element.addEventListener("beforeinput", (event) => {
+ beforeInputFired = true;
+ ok(event instanceof InputEvent,
+ '"beforeinput" event should be dispatched with InputEvent interface');
+ is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
+ `"beforeinput" event should be cancelable unless it's disabled by the pref`);
+ is(event.bubbles, true,
+ '"beforeinput" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "beforeinput" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "beforeinput" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "beforeinput" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "beforeinput" event should return empty array');
+ }, {once: true});
+ element.addEventListener("input", (event) => {
+ ok(beforeInputFired, `"beforeinput" event should've been fired before "input" on <${element.tagName} type="${element.type}">`);
+ ok(event instanceof InputEvent,
+ '"input" event should be dispatched with InputEvent interface');
+ is(event.cancelable, false,
+ '"input" event should be never cancelable');
+ is(event.bubbles, true,
+ '"input" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "input" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "input" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "input" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "input" should return empty array');
+ resolve();
+ }, {once: true})
+ });
+ synthesizeKey("KEY_Enter");
+ await promise;
+}
+
+// tgiles: We need this task due to timing issues between focusAndWaitForFieldsIdentified and popupShownListener.
+// There's a 300ms delay in focusAndWaitForFieldsIdentified that can cause triggerPopupAndHoverItem to get out of sync
+// and cause the popup to appear before the test expects a popup to appear.
+
+// Without this task we end up either getting a consistent timeout or getting the following exception:
+// 0:20.55 GECKO(31108) JavaScript error: , line 0: uncaught exception: Checking selected index - timed out after 50 tries.
+// This exception appears if you attempt to create the expectPopup promise earlier than it currently is in triggetPopupAndHoverItem
+add_task(async function a_dummy_task() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await triggerPopupAndHoverItem("#tel", 0);
+ await confirmClear("#tel");
+ await checkIsFormCleared({
+ "cc-exp-month": "MM",
+ "cc-exp-year": "YY"
+ });
+});
+
+add_task(async function clear_distinct_section() {
+ if (!(await canTestOSKeyStoreLogin())) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
+ await osKeyStoreLoginShown;
+
+ for (const [id, val] of Object.entries(MOCK_CC_STORAGE[0])) {
+ const element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+ checkFieldValue(element, val);
+ await checkFieldHighlighted(element, true);
+ }
+
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ await confirmClear("#cc-name");
+ await checkIsFormCleared({
+ "cc-exp-month": "MM",
+ "cc-exp-year": "YY"
+ });
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" autocomplete="organization"></label></p>
+ <p><label>streetAddress: <input id="street-address" autocomplete="street-address"></label></p>
+ <p><label>tel: <input id="tel" autocomplete="tel"></label></p>
+ <p><label>country: <input id="country" autocomplete="country"></label></p>
+
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <!-- NOTE: If you're going to write a test like this,
+ ensure that the selected option doesn't match the data that you're trying to autofill,
+ otherwise your test will wait forever for an event that will never fire.
+ I.e, if your saved cc-exp-month is 01, make sure your selected option ISN'T 01.
+ -->
+ <p><label>Expiration month: <select id="cc-exp-month" autocomplete="cc-exp-month">
+ <option value="MM" selected>MM</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ </label></p>
+ <!-- NOTE: If you're going to write a test like this,
+ ensure that the selected option doesn't match the data that you're trying to autofill,
+ otherwise your test will wait forever for an event that will never fire.
+ I.e, if your saved cc-exp-year is 2017, make sure your selected option ISN'T 2017.
+ -->
+ <p><label>Expiration year: <select id="cc-exp-year" autocomplete="cc-exp-year">
+ <option value="YY" selected>YY</option>
+ <option value="2017">2017</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ </select>
+ </label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form credit card autofill
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+async function setupCreditCardStorage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+}
+
+async function setupFormHistory() {
+ await updateFormHistory([
+ {op: "add", fieldname: "cc-name", value: "John Smith"},
+ {op: "add", fieldname: "cc-number", value: "6011029476355493"},
+ ]);
+}
+
+initPopupListener();
+
+// Show Form History popup for non-autocomplete="off" field only
+add_task(async function history_only_menu_checking() {
+ await setupFormHistory();
+
+ await setInput("#cc-number", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["6011029476355493"], false);
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await notExpectPopup();
+});
+
+// Show Form Autofill popup for the credit card fields.
+add_task(async function check_menu_when_both_with_autocomplete_off() {
+ await setupCreditCardStorage();
+
+ await setInput("#cc-number", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primaryAffix: cc.ccNumberFmt.affix,
+ primary: cc.ccNumberFmt.label,
+ secondary: cc["cc-name"],
+ ariaLabel: `${getCCTypeName(cc)} ${cc.ccNumberFmt.affix} ${cc.ccNumberFmt.label} ${cc["cc-name"]}`,
+ })));
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <form id="form1">
+ <p>This is a Credit Card form with autocomplete="off" cc-name field.</p>
+ <p><label>Name: <input id="cc-name" autocomplete="off"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ </form>
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - preview and highlight with multiple cc number fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: preview and highlight multiple cc number fields
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const MOCK_STORAGE_EXPECTED_FILL = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": "04",
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": "12",
+ "cc-exp-year": 2022,
+}]
+
+const MOCK_STORAGE_PREVIEW = [{
+ "cc-name": "Test Name",
+ "cc-number": "************1045",
+ "cc-exp-month": "04",
+ "cc-exp-year": "2017",
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "************7870",
+ "cc-exp-month": "12",
+ "cc-exp-year": "2022",
+}];
+
+
+/*
+ This function is similar to checkFormFieldsStyle in formautofill_common.js, but deals with the case
+ when one value is spread across multiple fields.
+
+ This function is needed because of the multiple cc-number filling behavior introduced in Bug 1688607.
+ Since the cc-number is stored as a whole value in the profile,
+ there has to be specific handling in the test to assert the correct fillable value.
+ Otherwise, we would try to grab a value out of profile["cc-number1"] which doesn't exist.
+*/
+async function checkMultipleCCNumberFormStyle(profile, isPreviewing = true) {
+ const elements = document.querySelectorAll("input, select");
+ for (const element of elements) {
+ let fillableValue;
+ if (element.id.includes("cc-number") && isPreviewing) {
+ fillableValue = profile["cc-number"].slice(-8);
+ } else if (element.id.includes("cc-number")) {
+ fillableValue = profile["cc-number"];
+ } else {
+ fillableValue = profile[element.id];
+ }
+ let previewValue = (isPreviewing && fillableValue) || "";
+ await checkFieldHighlighted(element, !!fillableValue);
+ await checkFieldPreview(element, previewValue);
+
+ }
+}
+
+/*
+ This function sets up 'change' event listeners so that we can safely
+ assert an element's value after autofilling has occurred.
+ This is essentially a barebones copy of triggerAutofillAndCheckProfile
+ that exists in formautofill_common.js.
+
+ We can't use triggerAutofillAndCheckProfile because "cc-number1" through "cc-number4"
+ do not exist in the profile.
+ Again, we store the whole cc-number in the profile, not its subsections.
+ So if we tried to grab the element by ID using "cc-number", this element would not exist in the doc,
+ causing triggerAutofillAndCheckProfile to throw an exception.
+*/
+async function setupListeners(elements, profile) {
+ for (const element of elements) {
+ let id = element.id;
+ element.addEventListener("change", () => {
+ let filledValue;
+ if (id == "cc-number1") {
+ filledValue = profile["cc-number"].slice(0, 4);
+ } else if (id == "cc-number2") {
+ filledValue = profile["cc-number"].slice(4, 8);
+ } else if (id == "cc-number3") {
+ filledValue = profile["cc-number"].slice(8, 12);
+ } else if (id == "cc-number4") {
+ filledValue = profile["cc-number"].slice(12, 16);
+ } else {
+ filledValue = profile[element.id];
+ }
+ checkFieldValue(element, filledValue);
+ }, {once: true})
+ }
+
+}
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+});
+
+add_task(async function check_preview() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let popup = expectPopup();
+ const focusedInput = await setInput("#cc-name", "");
+ await popup;
+ for (let i = 0; i < MOCK_STORAGE_PREVIEW.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(i);
+ await checkMultipleCCNumberFormStyle(MOCK_STORAGE_PREVIEW[i]);
+ }
+
+ focusedInput.blur();
+});
+
+add_task(async function check_filled_highlight() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ // filled 1st credit card option
+ synthesizeKey("KEY_Enter");
+ await osKeyStoreLoginShown;
+ let elements = document.querySelectorAll("input, select");
+ let profile = MOCK_STORAGE_EXPECTED_FILL[0];
+ await setupListeners(elements, profile);
+ await checkMultipleCCNumberFormStyle(profile, false);
+});
+</script>
+<p id="display"></p>
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic credit card form.</p>
+ <p>card number subsection 1: <input id="cc-number1" maxlength="4"></p>
+ <p>card number subsection 2: <input id="cc-number2" maxlength="4"></p>
+ <p>card number subsection 3: <input id="cc-number3" maxlength="4"></p>
+ <p>card number subsection 4: <input id="cc-number4" maxlength="4"></p>
+ <p>cardholder name: <input id="cc-name" autocomplete="cc-name"></p>
+ <p>expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></p>
+ <p>expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></p>
+ </form>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - preview and highlight with site prefill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: preview and highlight field that has been filled by site
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const MOCK_STORAGE_PREVIEW = [{
+ "cc-name": "Test Name",
+ "cc-number": "************1045",
+ "cc-exp-month": "04",
+ "cc-exp-year": "2017",
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "************7870",
+ "cc-exp-month": "12",
+ "cc-exp-year": "2022",
+}];
+
+const MOCK_STORAGE_EXPECTED_FILL = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": "04",
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": "12",
+ "cc-exp-year": 2022,
+}];
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+});
+
+add_task(async function check_preview() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ let cardholderName = document.querySelector("#cc-name");
+ let sitePrefillValue = cardholderName.value;
+ let popup = expectPopup();
+ const focusedInput = await setInput("#cc-number", "");
+ await popup;
+ for (let i = 0; i < MOCK_STORAGE_PREVIEW.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(i);
+ await checkFormFieldsStyle(MOCK_STORAGE_PREVIEW[i]);
+ }
+
+ focusedInput.blur();
+ is(cardholderName.value, sitePrefillValue, "value should not have changed because previous value was a site prefill");
+});
+
+add_task(async function check_filled_highlight() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ await triggerPopupAndHoverItem("#cc-number", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ // filled 1st credit card option
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE_EXPECTED_FILL[0]);
+ await osKeyStoreLoginShown;
+ await checkFormFieldsStyle(MOCK_STORAGE_EXPECTED_FILL[0], false);
+});
+</script>
+<p id="display"></p>
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic credit card form.</p>
+ <p>card number: <input id="cc-number" autocomplete="cc-number"></p>
+ <p>cardholder name: <input id="cc-name" autocomplete="cc-name" value="JOHN DOE"></p>
+ <p>expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></p>
+ <p>expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></p>
+ </form>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submission for a country without address-level1</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: Test autofill submission for a country without address-level1
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+const TEST_ADDRESSES = [{
+ organization: "Mozilla",
+ "street-address": "123 Sesame Street",
+ "address-level1": "AL",
+ country: "DE",
+ timesUsed: 1,
+}];
+
+add_task(async function test_DE_is_valid_testcase() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supportedCountries", "US,CA,DE"],
+ ["extensions.formautofill.creditCards.supportedCountries", "US,CA,DE"],
+ ],
+ });
+ let chromeScript = SpecialPowers.loadChromeScript(function test_country_data() {
+ /* eslint-env mozilla/chrome-script */
+ const {AddressDataLoader} = ChromeUtils.importESModule("resource://gre/modules/shared/FormAutofillUtils.sys.mjs");
+ let data = AddressDataLoader.getData("DE");
+ addMessageListener("CheckSubKeys", () => {
+ return !data.defaultLocale.sub_keys;
+ });
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.destroy();
+ });
+
+ let result = await chromeScript.sendQuery("CheckSubKeys");
+ ok(result, "Check that there are no sub_keys for the test country");
+});
+
+add_task(async function test_form_will_submit_without_sub_keys() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // This needs to match the country in the previous test and must have no sub_keys.
+ ["browser.search.region", "DE"],
+ // We already verified the first time use case in browser test
+ ["extensions.formautofill.firstTimeUse", false],
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supportedCountries", "US,CA,DE"],
+ ["extensions.formautofill.addresses.supported", "detect"]
+ ],
+ });
+ // Click a field to get the form handler created
+ await focusAndWaitForFieldsIdentified("input[autocomplete='organization']");
+
+ let loadPromise = new Promise(resolve => {
+ /* eslint-disable-next-line mozilla/balanced-listeners */
+ document.getElementById("submit_frame").addEventListener("load", resolve);
+ });
+
+ clickOnElement("input[type=submit]");
+ await onStorageChanged("add");
+ // Check if timesUsed is set correctly
+ let matching = await checkAddresses(TEST_ADDRESSES);
+ ok(matching, "Address saved as expected");
+
+ await loadPromise;
+ isnot(window.submit_frame.location.href, "about:blank", "Check form submitted");
+});
+
+</script>
+
+<div>
+ <!-- Submit to the frame so that the test doesn't get replaced. We don't return
+ -- false in onsubmit since we're testing the submission succeeds. -->
+ <iframe id="submit_frame" name="submit_frame"></iframe>
+ <form action="/" target="submit_frame" method="POST">
+ <p><label>organization: <input autocomplete="organization" value="Mozilla"></label></p>
+ <p><label>streetAddress: <input autocomplete="street-address" value="123 Sesame Street"></label></p>
+ <p><label>address-level1: <select autocomplete="address-level1">
+ <option selected>AL</option>
+ <option>AK</option>
+ </select></label></p>
+ <p><label>country: <input autocomplete="country" value="DE"></label></p>
+ <p><input type="submit"></p>
+ </form>
+
+</div>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submit</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+SpecialPowers.pushPrefEnv({"set": [["security.allow_eval_with_system_principal", true]]});
+
+let MOCK_STORAGE = [{
+ "given-name": "John",
+ "additional-name": "R",
+ "family-name": "Smith",
+ "organization": "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ "tel": "+13453453456",
+ "country": "US",
+ "address-level1": "NY",
+}];
+
+initPopupListener();
+
+add_task(async function setupStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+
+ await updateFormHistory([
+ {op: "add", fieldname: "username", value: "petya"},
+ {op: "add", fieldname: "current-password", value: "abrh#25_,K"},
+ ]);
+});
+
+add_task(async function check_switch_autofill_form_popup() {
+ await setInput("#tel", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(
+ [
+ `{"primary":"+13453453456","secondary":"123 Sesame Street."}`,
+ `{"primary":"","secondary":"","categories":["name","organization","address","tel"],"focusedCategory":"tel"}`,
+ ],
+ false
+ );
+
+ await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)");
+});
+
+add_task(async function check_switch_oridnal_form_popup() {
+ // We need an intentional wait here before switching form.
+ await sleep();
+ await setInput("#username", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["petya"], false);
+
+ await testMenuEntry(0, "el instanceof MozElements.MozAutocompleteRichlistitem");
+});
+
+add_task(async function check_switch_autofill_form_popup_back() {
+ // We need an intentional wait here before switching form.
+ await sleep();
+ await setInput("#tel", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(
+ [
+ `{"primary":"+13453453456","secondary":"123 Sesame Street."}`,
+ `{"primary":"","secondary":"","categories":["name","organization","address","tel"],"focusedCategory":"tel"}`,
+ ],
+ false
+ );
+
+ await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)");
+});
+
+</script>
+
+<div>
+
+ <h2>Address form</h2>
+ <form class="alignedLabels">
+ <label>given-name: <input autocomplete="given-name" autofocus></label>
+ <label>additional-name: <input id="additional-name" autocomplete="additional-name"></label>
+ <label>family-name: <input autocomplete="family-name"></label>
+ <label>organization: <input autocomplete="organization"></label>
+ <label>street-address: <input autocomplete="street-address"></label>
+ <label>address-level1: <input autocomplete="address-level1"></label>
+ <label>postal-code: <input autocomplete="postal-code"></label>
+ <label>country: <input autocomplete="country"></label>
+ <label>country-name: <input autocomplete="country-name"></label>
+ <label>tel: <input id="tel" autocomplete="tel"></label>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+
+ <h2>Ordinal form</h2>
+ <form class="alignedLabels">
+ <label>username: <input id="username" autocomplete="username"></label>
+ <p><input type="submit" value="Username"></p>
+ </form>
+
+</div>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: autocomplete on an autofocus form
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ tel: "1-345-345-3456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+ tel: "1-650-903-0800",
+}];
+
+initPopupListener();
+
+async function setupAddressStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+ await addAddress(MOCK_STORAGE[1]);
+}
+
+add_task(async function check_autocomplete_on_autofocus_field() {
+ await setupAddressStorage();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({primary: address.organization, secondary: address["street-address"]})
+ ));
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
+ <script>
+ "use strict";
+ // Focuses the input before DOMContentLoaded
+ document.getElementById("organization").focus();
+ </script>
+ <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>country: <input id="country" name="country" autocomplete="country" type="text"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form address autofill
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.\n2-line\n3-line",
+ tel: "+13453453456",
+ country: "US",
+ "address-level1": "NY",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue\n2-line\n3-line",
+ tel: "+16509030800",
+ country: "US",
+ "address-level1": "CA",
+}];
+
+async function setupAddressStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+ await addAddress(MOCK_STORAGE[1]);
+}
+
+async function setupFormHistory() {
+ await updateFormHistory([
+ {op: "add", fieldname: "tel", value: "+1234567890"},
+ {op: "add", fieldname: "email", value: "foo@mozilla.com"},
+ ]);
+}
+
+initPopupListener();
+
+// Form with history only.
+add_task(async function history_only_menu_checking() {
+ await setupFormHistory();
+
+ await setInput("#tel", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["+1234567890"], false);
+});
+
+// Display history search result if less than 3 inputs are covered by all saved
+// fields in the storage.
+add_task(async function all_saved_fields_less_than_threshold() {
+ await addAddress({
+ email: "test@test.com",
+ });
+
+ await setInput("#email", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["foo@mozilla.com"], false);
+
+ await cleanUpAddresses();
+});
+
+// Form with both history and address storage.
+add_task(async function check_menu_when_both_existed() {
+ await setupAddressStorage();
+
+ await setInput("#organization", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.organization,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+
+ await setInput("#street-address", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ secondary: address.organization,
+ })
+ ));
+
+ await setInput("#tel", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.tel,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+
+ await setInput("#address-line1", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ secondary: address.organization,
+ })
+ ));
+});
+
+// Display history search result if no matched data in addresses.
+add_task(async function check_fallback_for_mismatched_field() {
+ await setInput("#email", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["foo@mozilla.com"], false);
+});
+
+// Display history search result if address autofill is disabled.
+add_task(async function check_search_result_for_pref_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.formautofill.addresses.enabled", false]],
+ });
+
+ await setInput("#tel", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["+1234567890"], false);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Autofill the address from dropdown menu.
+add_task(async function check_fields_after_form_autofill() {
+ const focusedInput = await setInput("#organization", "Moz");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.organization,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ).slice(1));
+ synthesizeKey("KEY_ArrowDown");
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+ synthesizeKey("KEY_Escape");
+ is(focusedInput.value, "Mozilla", "Filled field shouldn't be reverted by ESC key");
+});
+
+// Fallback to history search after autofill address.
+add_task(async function check_fallback_after_form_autofill() {
+ await setInput("#tel", "", true);
+ await triggerPopupAndHoverItem("#tel", 0);
+ checkMenuEntries(["+1234567890"], false);
+ await triggerAutofillAndCheckProfile({
+ tel: "+1234567890",
+ });
+});
+
+// Resume form autofill once all the autofilled fileds are changed.
+add_task(async function check_form_autofill_resume() {
+ document.querySelector("#tel").blur();
+ document.querySelector("#form1").reset();
+ await setInput("#tel", "");
+ await triggerPopupAndHoverItem("#tel", 0);
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.tel,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[0]);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1" name="address-line1" autocomplete="address-line1" type="text"></label></p>
+ <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>email: <input id="email" name="email" autocomplete="email" type="text"></label></p>
+ <p><label>country: <select id="country" name="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ </select></label></p>
+ <p><label>states: <select id="address-level1" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="CA">California</option>
+ <option value="NY">New York</option>
+ <option value="WA">Washington</option>
+ </select></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: autocomplete on an autofocus form
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ name: "John Doe",
+ organization: "Sesame Street",
+ "address-level2": "Austin",
+ tel: "+13453453456",
+}, {
+ name: "Foo Bar",
+ organization: "Mozilla",
+ "address-level2": "San Francisco",
+ tel: "+16509030800",
+}];
+
+initPopupListener();
+
+async function setupAddressStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+ await addAddress(MOCK_STORAGE[1]);
+}
+
+function addInputField(form, className) {
+ let newElem = document.createElement("input");
+ newElem.name = className;
+ newElem.autocomplete = className;
+ newElem.type = "text";
+ form.appendChild(newElem);
+}
+
+async function checkFieldsAutofilled(formId, profile) {
+ const elements = document.querySelectorAll(`#${formId} input`);
+ for (const element of elements) {
+ await SimpleTest.promiseWaitForCondition(() => {
+ return element.value == profile[element.name];
+ });
+ await checkFieldHighlighted(element, true);
+ }
+}
+
+async function checkFormChangeHappened(formId) {
+ info("expecting form changed");
+ await focusAndWaitForFieldsIdentified(`#${formId} input[name=tel]`);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({primary: address.tel, secondary: address.name})
+ ));
+
+ // Click the first entry of the autocomplete popup and make sure all fields are autofilled
+ synthesizeKey("KEY_Enter");
+ await checkFieldsAutofilled(formId, MOCK_STORAGE[0]);
+ // This is for checking the changes of element count.
+ addInputField(document.querySelector(`#${formId}`), "address-level2");
+
+ await focusAndWaitForFieldsIdentified(`#${formId} input[name=name]`);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ // Click on an autofilled field would show an autocomplete popup with "clear form" entry
+ checkMenuEntries([
+ JSON.stringify({primary: "", secondary: ""}), // Clear Autofill Form
+ JSON.stringify({primary: "", secondary: ""}) // FormAutofill Preferemce
+ ], false);
+
+ // This is for checking the changes of element removed and added then.
+ document.querySelector(`#${formId} input[name=address-level2]`).remove();
+ addInputField(document.querySelector(`#${formId}`), "address-level2");
+
+ await focusAndWaitForFieldsIdentified(`#${formId} input[name=address-level2]`, true);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({primary: address["address-level2"], secondary: address.name})
+ ));
+
+ // Make sure everything is autofilled in the end
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await checkFieldsAutofilled(formId, MOCK_STORAGE[0]);
+}
+
+add_task(async function init_storage() {
+ await setupAddressStorage();
+});
+
+add_task(async function check_change_happened_in_form() {
+ await checkFormChangeHappened("form1");
+});
+
+add_task(async function check_change_happened_in_body() {
+ await checkFormChangeHappened("form2");
+});
+</script>
+
+<p id="display"></p>
+<div id="content">
+ <form id="form1">
+ <p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
+ </form>
+ <div id="form2">
+ <p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
+ </div>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - preview and highlight</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: preview and highlight
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+const MOCK_STORAGE = [
+ {
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ tel: "+13453453456",
+ },
+ {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+ },
+ {
+ organization: "Tel org",
+ tel: "+12223334444",
+ },
+ {
+ organization: "Random Org",
+ "address-level1": "First Admin Level",
+ tel: "+13453453456",
+ },
+ {
+ organization: "readonly Org",
+ "address-level1": "First Admin Level",
+ tel: "+13453453456",
+ name: "John Doe",
+ },
+ {
+ organization: "test org",
+ "address-level2": "Not a Town",
+ tel: "+13453453456",
+ name: "John Doe",
+ }
+];
+
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ for (const storage of MOCK_STORAGE) {
+ await addAddress(storage);
+ }
+});
+
+add_task(async function check_preview() {
+ const focusedInput = await setInput("#organization", "");
+
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ await checkFormFieldsStyle(null);
+
+ for (let i = 0; i < MOCK_STORAGE.length; i++) {
+ info(`Checking organization: ${MOCK_STORAGE[i].organization} preview`);
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(i);
+ await checkFormFieldsStyle(MOCK_STORAGE[i]);
+ }
+
+ // Navigate to the footer
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(MOCK_STORAGE.length);
+ await checkFormFieldsStyle(null);
+
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(-1);
+ await checkFormFieldsStyle(null);
+
+ focusedInput.blur();
+});
+
+add_task(async function check_filled_highlight() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ // filled 1st address
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[0]);
+ await checkFormFieldsStyle(MOCK_STORAGE[0], false);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" autocomplete="organization"></label></p>
+ <p><label>streetAddress: <input id="street-address" autocomplete="street-address"></label></p>
+ <p><label>tel: <input id="tel" autocomplete="tel"></label></p>
+ <p><label>country: <input id="country" autocomplete="country"></label></p>
+ <p><label>address-level1:
+ <select id="address-level1" autocomplete="address-level1">
+ <option>First Admin Level</option>
+ <option>Second Admin Level</option>
+ </select>
+ </label></p>
+ <p><label>full name: <input id="name" autocomplete="name" readonly value="UNCHANGED"></label></p>
+ <p><label>address-level2: <input id="address-level2" autocomplete="address-level2" disabled value="Town"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form address autofill
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ organization: "Mozilla Vancouver",
+ "street-address": "163 W Hastings St.\n#209\n3-line",
+ tel: "+17787851540",
+ country: "CA",
+ "address-level1": "BC",
+}, {
+ organization: "Mozilla Toronto",
+ "street-address": "366 Adelaide St.\nW Suite 500\n3-line",
+ tel: "+14168483114",
+ country: "CA",
+ "address-level1": "ON",
+}, {
+ organization: "Prince of Wales Northern Heritage",
+ "street-address": "4750 48 St.\nYellowknife\n3-line",
+ tel: "+18677679347",
+ country: "CA",
+ "address-level1": "Northwest Territories",
+}, {
+ organization: "ExpoCité",
+ "street-address": "250 Boulevard Wilfrid-Hamel\nVille de Québec\n3-line",
+ tel: "+14186917110",
+ country: "CA",
+ "address-level1": "Québec",
+}];
+
+function checkElementFilled(element, expectedvalue) {
+ let focusFired = false;
+ let inputFired = false;
+ let changeFired = false;
+ return [
+ new Promise(resolve => {
+ element.addEventListener("focus", function onChange() {
+ ok(true, "Checking " + element.name + " field fires focus event");
+ focusFired = true;
+ resolve();
+ }, {once: true});
+ }),
+ new Promise(resolve => {
+ let beforeInputFired = false;
+ let oldValue = element.value;
+ element.addEventListener("beforeinput", function onBeforeInput(event) {
+ ok(true, "Checking " + element.name + " field fires beforeinput event");
+ ok(focusFired, "Focus fired before `beforeinput` event");
+ beforeInputFired = true;
+ ok(event instanceof InputEvent,
+ `"beforeinput" event should be dispatched with InputEvent interface on ${element.name}`);
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "beforeinput" event should be "insertReplacementText"');
+ is(event.data, expectedvalue,
+ 'data value of "beforeinput" event should be same as expected value');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "beforeinput" event should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "beforeinput" event 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.name} unless it's suppressed by the pref`);
+ is(event.bubbles, true,
+ `"input" event should always bubble on ${element.name}`);
+ is(element.value, oldValue,
+ 'value of the element should not be modified at "beforeinput" event yet');
+ }, {once: true});
+ element.addEventListener("input", function onInput(event) {
+ ok(true, "Checking " + element.name + " field fires input event");
+ if (element.tagName == "INPUT" && element.type == "text") {
+ ok(beforeInputFired, `"beforeinput" event shoud've been fired on ${element.name} before "input" event`);
+ ok(event instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface on ${element.name}`);
+ is(event.inputType, "insertReplacementText",
+ "inputType value should be \"insertReplacementText\"");
+ is(event.data, expectedvalue,
+ "data value should be same as expected value");
+ is(event.dataTransfer, null,
+ "dataTransfer value should be null");
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() should return empty array');
+ is(element.value, expectedvalue,
+ 'value of the element should be modified at "input" event');
+ } else {
+ ok(!beforeInputFired, `"beforeinput" event shoudn't be fired on ${element.name} before "input" event`);
+ ok(event instanceof Event && !(event instanceof UIEvent),
+ `"input" event should be dispatched with Event interface on ${element.name}`);
+ }
+ is(event.cancelable, false,
+ `"input" event should be never cancelable on ${element.name}`);
+ is(event.bubbles, true,
+ `"input" event should always bubble on ${element.name}`);
+ inputFired = true;
+ resolve();
+ }, {once: true});
+ }),
+ new Promise(resolve => {
+ element.addEventListener("change", function onChange() {
+ ok(true, "Checking " + element.name + " field fires change event");
+ is(element.value, expectedvalue, "Checking " + element.name + " field");
+ ok(focusFired, "Focus fired before `change` event");
+ changeFired = true;
+ resolve();
+ }, {once: true});
+ }),
+ new Promise(resolve => {
+ element.addEventListener("blur", function onChange() {
+ ok(true, "Checking " + element.name + " field fires blur event");
+ ok(changeFired, "Change fired before `blur` event");
+ ok(inputFired, "Input fired before `blur` event");
+ is(element.value, expectedvalue, "Checking " + element.name + " field");
+ resolve();
+ }, {once: true});
+ }),
+ ];
+}
+
+function checkAutoCompleteInputFilled(element, expectedvalue) {
+ return new Promise(resolve => {
+ element.addEventListener("input", function onInput() {
+ is(element.value, expectedvalue, "Checking " + element.name + " field");
+ resolve();
+ }, {once: true});
+ });
+}
+
+function checkFormFilled(selector, address) {
+ info("expecting form filled");
+ let promises = [];
+ let form = document.querySelector(selector);
+ for (let prop in address) {
+ let element = form.querySelector(`[name=${prop}]`);
+ if (document.activeElement == element) {
+ promises.push(checkAutoCompleteInputFilled(element, address[prop]));
+ } else {
+ let converted = address[prop];
+ if (prop == "street-address") {
+ converted = FormAutofillUtils.toOneLineAddress(converted);
+ }
+ promises.push(...checkElementFilled(element, converted));
+ }
+ }
+ synthesizeKey("KEY_Enter");
+ return Promise.all(promises);
+}
+
+async function setupAddressStorage() {
+ for (let address of MOCK_STORAGE) {
+ await addAddress(address);
+ }
+}
+
+initPopupListener();
+
+add_setup(async () => {
+ // This test relies on being able to fill a Canadian address which isn't possible
+ // without `supportedCountries` allowing Canada
+ await SpecialPowers.pushPrefEnv({"set": [["extensions.formautofill.supportedCountries", "US,CA"]]});
+
+ await setupAddressStorage();
+});
+
+// Autofill the address with address level 1 code.
+add_task(async function autofill_with_level1_code() {
+ await setInput("#organization-en", "Mozilla Toront");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in English for test result
+ let result = Object.assign({}, MOCK_STORAGE[1], {"address-level1": "Ontario"});
+ await checkFormFilled("#form-en", result);
+
+ await setInput("#organization-fr", "Mozilla Vancouve");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in French for test result
+ result = Object.assign({}, MOCK_STORAGE[0], {"address-level1": "Colombie-Britannique"});
+ await checkFormFilled("#form-fr", result);
+ document.querySelector("#form-en").reset();
+ document.querySelector("#form-fr").reset();
+});
+
+// Autofill the address with address level 1 full name.
+add_task(async function autofill_with_level1_full_name() {
+ await setInput("#organization-en", "ExpoCit");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in French for test result
+ let result = Object.assign({}, MOCK_STORAGE[3], {"address-level1": "Quebec"});
+ await checkFormFilled("#form-en", result);
+
+ await setInput("#organization-fr", "Prince of Wales");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in English for test result
+ result = Object.assign({}, MOCK_STORAGE[2], {"address-level1": "Territoires du Nord-Ouest"});
+ await checkFormFilled("#form-fr", result);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form-en">
+ <p>This is a basic CA form with en address level 1 select.</p>
+ <p><label>organization: <input id="organization-en" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address-en" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1-en" name="address-line1" autocomplete="address-line1" type="text"></label></p>
+ <p><label>tel: <input id="tel-en" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>email: <input id="email-en" name="email" autocomplete="email" type="text"></label></p>
+ <p><label>country: <select id="country-en" name="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ <option value="CA">Canada</option>
+ </select></label></p>
+ <p><label>states: <select id="address-level1-en" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="British Columbia">British Columbia</option>
+ <option value="Ontario">Ontario</option>
+ <option value="Northwest Territories">Northwest Territories</option>
+ <option value="Quebec">Quebec</option>
+ </select></label></p>
+ </form>
+
+ <form id="form-fr">
+ <p>This is a basic CA form with fr address level 1 select.</p>
+ <p><label>organization: <input id="organization-fr" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address-fr" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1-fr" name="address-line1" autocomplete="address-line1" type="text"></label></p>
+ <p><label>tel: <input id="tel-fr" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>email: <input id="email-fr" name="email" autocomplete="email" type="text"></label></p>
+ <p><label>country: <select id="country-fr" name="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ <option value="CA">Canada</option>
+ </select></label></p>
+ <p><label>states: <select id="address-level1-fr" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="Colombie-Britannique">Colombie-Britannique</option>
+ <option value="Ontario">Ontario</option>
+ <option value="Territoires du Nord-Ouest">Territoires du Nord-Ouest</option>
+ <option value="Québec">Québec</option>
+ </select></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submit</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ "given-name": "John",
+ "additional-name": "R",
+ "family-name": "Smith",
+}];
+
+initPopupListener();
+
+add_task(async function setupStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+});
+
+add_task(async function check_switch_form_popup() {
+ await setInput("#additional-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ // We need an intentional wait here before switching form.
+ await sleep();
+ await setInput("#organization", "");
+ synthesizeKey("KEY_ArrowDown");
+ const {open: popupOpen} = await getPopupState();
+ is(popupOpen, false);
+
+ await sleep();
+ await setInput("#given-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+});
+
+</script>
+
+<div>
+
+ <form>
+ <label>Name:<input id="name" autocomplete="name"></label>
+ <label>Organization:<input id="organization" autocomplete="organization"></label>
+ <label>City:<input autocomplete="address-level2"></label>
+ </form>
+
+ <form>
+ <label>Given-Name: <input id="given-name" autocomplete="given-name"></label>
+ <label>Additional-Name/Middle: <input id="additional-name" autocomplete="additional-name"></label>
+ <label>FamilyName-LastName: <input id="family-name" autocomplete="family-name"></label>
+ </form>
+
+</div>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submit</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: check if address is saved/updated correctly
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let TEST_ADDRESSES = [{
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ tel: "+13453453456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+ tel: "+16509030800",
+}];
+
+add_task(async function setup_prefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.enabled", true],
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ],
+ });
+});
+
+initPopupListener();
+
+// Submit first address for saving.
+add_task(async function check_storage_after_form_submitted() {
+ // We already verified the first time use case in browser test
+ await SpecialPowers.pushPrefEnv({
+ "set": [["extensions.formautofill.firstTimeUse", false]],
+ });
+
+ for (let key in TEST_ADDRESSES[0]) {
+ await setInput("#" + key, TEST_ADDRESSES[0][key]);
+ }
+
+ clickOnElement("input[type=submit]");
+
+ let expectedAddresses = TEST_ADDRESSES.slice(0, 1);
+ await onStorageChanged("add");
+ // Check if timesUsed is set correctly
+ expectedAddresses[0].timesUsed = 1;
+ let matching = await checkAddresses(expectedAddresses);
+ ok(matching, "Address saved as expected");
+ delete expectedAddresses[0].timesUsed;
+});
+
+// Submit another new address.
+add_task(async function check_storage_after_another_address_submitted() {
+ await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
+
+ document.querySelector("form").reset();
+ for (let key in TEST_ADDRESSES[1]) {
+ await setInput("#" + key, TEST_ADDRESSES[1][key]);
+ }
+
+ clickOnElement("input[type=submit]");
+
+ // The 2nd test address should be on the top since it's the last used one.
+ let addressesInMenu = TEST_ADDRESSES.slice(1);
+ addressesInMenu.push(TEST_ADDRESSES[0]);
+
+ // let expectedAddresses = TEST_ADDRESSES.slice(0);
+ await onStorageChanged("add");
+ let matching = await checkAddresses(TEST_ADDRESSES);
+ ok(matching, "New address saved as expected");
+
+ await setInput("#organization", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(addressesInMenu.map(address =>
+ JSON.stringify({primary: address.organization, secondary: address["street-address"]})
+ ));
+});
+
+// Submit another new address that is mergeable.
+add_task(async function new_address_submitted_and_merged() {
+ // TODO: Bug Bug 1812294
+});
+
+// Submit an updated autofill address and merge.
+add_task(async function check_storage_after_form_submitted() {
+ // TODO: Bug Bug 1812294
+});
+
+// Submit a subset address manually.
+add_task(async function submit_subset_manually() {
+ // TODO: Bug Bug 1812294
+});
+
+</script>
+
+<div>
+
+ <form onsubmit="return false">
+ <p>This is a basic form for submitting test.</p>
+ <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>country: <input id="country" name="country" autocomplete="country" type="text"></label></p>
+ <p><input type="submit"></p>
+ </form>
+
+</div>
+</body>
+</html>
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: `<form><input id="given-name"><input id="family-name">
+ <input id="street-addr"><input id="city"><select id="country"></select>
+ <input id='email'><input id="tel"></form>`,
+ focusedInputId: "given-name",
+ profileData: {},
+ expectedResult: {
+ "street-addr": "",
+ city: "",
+ country: "",
+ email: "",
+ tel: "",
+ },
+ },
+ {
+ description: "Form with autocomplete properties and 1 token",
+ document: `<form><input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <select id="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ </select>
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel"></form>`,
+ 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: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="shipping street-address">
+ <input id="city" autocomplete="shipping address-level2">
+ <select id="country" autocomplete="shipping country">
+ <option/>
+ <option value="US">United States</option>
+ </select>
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ 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: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="shipping street-address">
+ <input id="city" autocomplete="shipping address-level2">
+ <input id="country" autocomplete="shipping country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ 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: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="billing street-address">
+ <input id="city" autocomplete="billing address-level2">
+ <input id="country" autocomplete="billing country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="off">
+ <input id="family-name" autocomplete="off">
+ <input id="street-address" autocomplete="off">
+ <input id="organization" autocomplete="off">
+ <input id="country" autocomplete="off">
+ </form>`,
+ 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: `<form autocomplete="off">
+ <input id="given-name">
+ <input id="family-name">
+ <input id="street-address">
+ <input id="city">
+ <input id="country">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <select id="country" autocomplete="shipping country">
+ <option value=""></option>
+ <option value="US">United States</option>
+ </select>
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">California</option>
+ <option value="WA">Washington</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <select id="country" autocomplete="shipping country">
+ <option value=""></option>
+ <option value="US">United States</option>
+ </select>
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">California</option>
+ <option value="WA">Washington</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2" readonly value="TEST CITY">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <select id="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ </select>
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <select id="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ </select>
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month" placeholder="MM">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>
+ `,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>
+ `,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year" placeholder="YY">
+ </form>
+ `,
+ 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: `<form>
+ <input id="hidden-cc" autocomplete="cc-number" hidden>
+ <input id="hidden-cc-2" autocomplete="cc-number" style="display:none">
+ <input id="visible-cc" autocomplete="cc-number">
+ <input id="hidden-name" autocomplete="cc-name" hidden>
+ <input id="hidden-name-2" autocomplete="cc-name" style="display:none">
+ <input id="visible-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name" value="JOHN DOE">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-type1">
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-exp-month1">
+ <input id="cc-exp-year1">
+ <input id="cc-type2">
+ <input id="cc-number5" maxlength="4">
+ <input id="cc-number6" maxlength="4">
+ <input id="cc-number7" maxlength="4">
+ <input id="cc-number8" maxlength="4">
+ <input id="cc-exp-month2">
+ <input id="cc-exp-year2">
+ <input>
+ <input>
+ <input>
+ </form>
+ `,
+ 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: `<form>
+ <input id="cardHolder">
+ <input id="cardNumber">
+ <input id="month" type="tel" name="month" placeholder="MM">
+ <input id="year" type="tel" name="year" placeholder="YY">
+ </form>
+ `,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <select id="country" autocomplete="shipping country">
+ <option value="US">United States</option>
+ </select>
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">California</option>
+ <option value="WA">Washington</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">California</option>
+ </select></form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="ca">ca</option>
+ </select></form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">CA</option>
+ </select></form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="US-WA">WA-Washington</option>
+ </select></form>`,
+ focusedInputId: "given-name",
+ profileData: {
+ guid: "123",
+ country: "US",
+ "address-level1": "WA",
+ },
+ expectedResult: {
+ state: "US-WA",
+ },
+ },
+
+ // Country
+ {
+ description: "Form with country select elements",
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <select id="country" autocomplete="country">
+ <option value=""></option>
+ <option value="US">United States</option>
+ </select></form>`,
+ focusedInputId: "given-name",
+ profileData: {
+ guid: "123",
+ country: "US",
+ },
+ expectedResult: {
+ country: "US",
+ },
+ },
+ {
+ description: "Form with country select elements; with lower case key",
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <select id="country" autocomplete="country">
+ <option value=""></option>
+ <option value="us">us</option>
+ </select></form>`,
+ focusedInputId: "given-name",
+ profileData: {
+ guid: "123",
+ country: "US",
+ },
+ expectedResult: {
+ country: "us",
+ },
+ },
+ {
+ description: "Form with country select elements; with alternative name 1",
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <select id="country" autocomplete="country">
+ <option value=""></option>
+ <option value="XX">United States</option>
+ </select></form>`,
+ focusedInputId: "given-name",
+ profileData: {
+ guid: "123",
+ country: "US",
+ },
+ expectedResult: {
+ country: "XX",
+ },
+ },
+ {
+ description: "Form with country select elements; with alternative name 2",
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <select id="country" autocomplete="country">
+ <option value=""></option>
+ <option value="XX">America</option>
+ </select></form>`,
+ focusedInputId: "given-name",
+ profileData: {
+ guid: "123",
+ country: "US",
+ },
+ expectedResult: {
+ country: "XX",
+ },
+ },
+ {
+ description:
+ "Form with country select elements; with partial matching value",
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <select id="country" autocomplete="country">
+ <option value=""></option>
+ <option value="XX">Ship to America</option>
+ </select></form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <select id="cc-exp-month" autocomplete="cc-exp-month">
+ <option value="">MM</option>
+ <option value="6">06</option>
+ </select></form>`,
+ 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: `<form>
+ <select id="cc-type" autocomplete="cc-type">
+ <option value="">Please select</option>
+ <option value="MA">Mastercard</option>
+ <option value="AX">American Express</option>
+ </select>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="country" autocomplete="country" disabled value="DE">
+ </form>`,
+ 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: `<form>
+ <input id="given-name">
+ <input id="family-name">
+ <input id="street-addr">
+ <input id="city">
+ </form>`,
+ 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: `<form>
+ <input id="given-name">
+ <input id="family-name">
+ <input id="street-addr">
+ <select id="state">
+ <option value="AL">Alabama</option>
+ <option value="AK">Alaska</option>
+ <option value="OH">Ohio</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="given-name">
+ <input id="family-name">
+ <input id="street-addr">
+ <select id="state">
+ <option value="AL">Alabama</option>
+ <option selected value="AK">Alaska</option>
+ <option value="OH">Ohio</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="given-name">
+ <input id="family-name">
+ <input id="street-addr">
+ <input id="city">
+ <select id="country"></select>
+ <input id='email'>
+ <input id="phone">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-address" autocomplete="street-address">
+ <input id="address-level2" autocomplete="address-level2">
+ <select id="country" autocomplete="country"></select>
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="address-level2" autocomplete="shipping address-level2">
+ <input id="country" autocomplete="shipping country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ 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: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="address-level2" autocomplete="shipping address-level2">
+ <select id="country" autocomplete="shipping country"></select>
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="cc-number" autocomplete="shipping cc-number">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input autocomplete="shipping address-level2">
+ </form>`,
+ sections: [],
+ },
+ /*
+ * Valid Credit Card Form with autocomplete attribute
+ */
+ {
+ description: "@autocomplete - A valid credit card form",
+ document: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ sections: [
+ [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-name" },
+ { fieldName: "cc-exp" },
+ ],
+ ],
+ },
+ {
+ description: "@autocomplete - A valid credit card form without cc-numner",
+ document: `<form>
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ sections: [[{ fieldName: "cc-name" }, { fieldName: "cc-exp" }]],
+ },
+ {
+ description: "@autocomplete - A valid cc-number only form",
+ document: `<form><input id="cc-number" autocomplete="cc-number"></form>`,
+ sections: [[{ fieldName: "cc-number" }]],
+ },
+ {
+ description: "@autocomplete - A valid cc-name only form",
+ document: `<form><input id="cc-name" autocomplete="cc-name"></form>`,
+ sections: [[{ fieldName: "cc-name" }]],
+ },
+ {
+ description: "@autocomplete - A valid cc-exp only form",
+ document: `<form><input id="cc-exp" autocomplete="cc-exp"></form>`,
+ sections: [[{ fieldName: "cc-exp" }]],
+ },
+ {
+ description: "@autocomplete - A valid cc-exp-month + cc-exp-year form",
+ document: `<form>
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ sections: [[{ fieldName: "cc-exp-month" }, { fieldName: "cc-exp-year" }]],
+ },
+ {
+ description: "@autocomplete - A valid cc-exp-month only form",
+ document: `<form><input id="cc-exp-month" autocomplete="cc-exp-month"></form>`,
+ sections: [[{ fieldName: "cc-exp-month" }]],
+ },
+ {
+ description: "@autocomplete - A valid cc-exp-year only form",
+ document: `<form><input id="cc-exp-year" autocomplete="cc-exp-year"></form>`,
+ 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: `<form>
+ <input id="cc-number" name="cc-number">
+ <input id="cc-name" name="cc-name">
+ <input id="cc-exp" name="cc-exp">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" name="cc-number">
+ <input id="cc-name" name="cc-name">
+ <input id="cc-exp" name="cc-exp">
+ </form>`,
+ 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: `<form>
+ <input id="cc-name" name="cc-name">
+ <input id="cc-exp" name="cc-exp">
+ </form>`,
+ 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: `<form><input id="cc-number" name="cc-number"></form>`,
+ 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: `<form><input id="cc-name" name="cc-name"></form>`,
+ 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 <input> in the form",
+ document: `<form><input id="cc-number" name="cc-number"></form>`,
+ 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 <input> in the form",
+ document: `<form><input id="cc-name" name="cc-name"></form>`,
+ 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: `<form>
+ <input id="cc-number" name="cc-number">
+ <input id="cc-name" name="cc-name">
+ <input id="cc-exp" name="cc-exp">
+ </form>`,
+ 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 <input> field",
+ document: `<form>
+ <input id="cc-name" name="cc-name">
+ <input id="password" type="password">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="shippingAC" name="phone" maxlength="3">
+ <input id="shippingPrefix" name="phone" maxlength="3">
+ <input id="shippingSuffix" name="phone" maxlength="4">
+ <input id="shippingTelExt" name="extension">
+
+ <input id="billingAC" name="phone" maxlength="3">
+ <input id="billingPrefix" name="phone" maxlength="3">
+ <input id="billingSuffix" name="phone" maxlength="4">
+
+ <input id="otherCC" name="phone" maxlength="3">
+ <input id="otherAC" name="phone" maxlength="3">
+ <input id="otherPrefix" name="phone" maxlength="3">
+ <input id="otherSuffix" name="phone" maxlength="4">
+ </form>`,
+ 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: `<form>
+ <input id="i1" autocomplete="given-name">
+ <input id="i2" autocomplete="family-name">
+ <input id="i3" autocomplete="street-address">
+ <input id="i4" autocomplete="email">
+
+ <input id="homePhone" maxlength="10">
+ <input id="mobilePhone" maxlength="10">
+ <input id="officePhone" maxlength="10">
+ </form>`,
+ 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: `<form>
+ <input id="i1" autocomplete="shipping given-name">
+ <input id="i2" autocomplete="shipping family-name">
+ <input id="i3" autocomplete="shipping street-address">
+ <input id="i4" autocomplete="shipping email">
+ <input id="singlePhone" autocomplete="shipping tel">
+ <input id="shippingAreaCode" autocomplete="shipping tel-area-code">
+ <input id="shippingPrefix" autocomplete="shipping tel-local-prefix">
+ <input id="shippingSuffix" autocomplete="shipping tel-local-suffix">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="dummyAreaCode" autocomplete="shipping tel" maxlength="3">
+ <input id="dummyPrefix" autocomplete="shipping tel" maxlength="3">
+ <input id="dummySuffix" autocomplete="shipping tel" maxlength="4">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="address-level1" autocomplete="address-level1">
+ <input id="country" autocomplete="country">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" autocomplete="country">
+ </form>`,
+ formValue: {
+ "given-name": "John",
+ organization: "Mozilla",
+ },
+ expectedRecord: {
+ address: [],
+ creditCard: [],
+ },
+ },
+ {
+ description: `"country" using @autocomplete shouldn't be identified aggressively`,
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" autocomplete="country">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" name="country">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="tel-country-code" autocomplete="tel-country-code">
+ <input id="tel-national" autocomplete="tel-national">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" autocomplete="country">
+ <input id="tel" autocomplete="tel-national">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" autocomplete="country">
+ <input id="tel" autocomplete="tel-national">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" autocomplete="country">
+ <input id="tel" autocomplete="tel-national">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="organization" autocomplete="organization">
+ </form>`,
+ 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: `<form>
+ <input id="tel-country-code" autocomplete="tel-country-code">
+ <input id="tel-area-code" autocomplete="tel-area-code">
+ <input id="tel-local" autocomplete="tel-local">
+ <input id="organization" autocomplete="organization">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="cc-type" autocomplete="cc-type">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ </form>`,
+ formValue: {
+ "cc-number": "4111111111111111",
+ },
+ expectedRecord: {
+ address: [],
+ creditCard: [
+ {
+ "cc-number": "4111111111111111",
+ },
+ ],
+ },
+ },
+ {
+ description: "A credit card form must have cc-number value.",
+ document: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ 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: `<form>
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ formValue: {
+ "cc-name": "Foo Bar",
+ "cc-exp": "2022-06",
+ },
+ expectedRecord: {
+ address: [],
+ creditCard: [],
+ },
+ },
+ {
+ description: "A form with multiple sections",
+ document: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="organization" autocomplete="organization">
+ <input id="country" autocomplete="country">
+
+ <input id="given-name-shipping" autocomplete="shipping given-name">
+ <input id="family-name-shipping" autocomplete="shipping family-name">
+ <input id="organization-shipping" autocomplete="shipping organization">
+ <input id="country-shipping" autocomplete="shipping country">
+
+ <input id="given-name-billing" autocomplete="billing given-name">
+ <input id="organization-billing" autocomplete="billing organization">
+ <input id="country-billing" autocomplete="billing country">
+
+ <input id="cc-number-section-one" autocomplete="section-one cc-number">
+ <input id="cc-name-section-one" autocomplete="section-one cc-name">
+
+ <input id="cc-number-section-two" autocomplete="section-two cc-number">
+ <input id="cc-name-section-two" autocomplete="section-two cc-name">
+ <input id="cc-exp-section-two" autocomplete="section-two cc-exp">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <label for="field1">Card Type:</label>
+ <select id="field1">
+ <option value="visa" selected>Visa</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <label for="cc-type">Card Type:</label>
+ <select id="cc-type">
+ <option value="V" selected>Visa</option>
+ <option value="A">American Express</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ 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: `<label id="typeA"> label type A
+ <!-- This comment should not be extracted. -->
+ <input type="text">
+ <script>FOO</script>
+ <noscript>FOO</noscript>
+ <option>FOO</option>
+ <style>FOO</style>
+ </label>`,
+ inputId: "typeA",
+ expectedStrings: ["label type A"],
+ },
+ {
+ description: "A label element with inner div contains one input element.",
+ document: `<label id="typeB"> label type B
+ <!-- This comment should not be extracted. -->
+ <script>FOO</script>
+ <noscript>FOO</noscript>
+ <option>FOO</option>
+ <style>FOO</style>
+ <div> inner div
+ <input type="text">
+ </div>
+ </label>`,
+ inputId: "typeB",
+ expectedStrings: ["label type B", "inner div"],
+ },
+ {
+ description:
+ "A label element with inner prefix/postfix strings contains span elements.",
+ document: `<label id="typeC"> label type C
+ <!-- This comment should not be extracted. -->
+ <script>FOO</script>
+ <noscript>FOO</noscript>
+ <option>FOO</option>
+ <style>FOO</style>
+ <div> inner div prefix
+ <span>test C-1 </span>
+ <span> test C-2</span>
+ inner div postfix
+ </div>
+ </label>`,
+ 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: `<form>
+ <label id="labelA"> label type A
+ <input id="typeA" type="text">
+ </label>
+ </form>`,
+ inputId: "typeA",
+ expectedLabelIds: ["labelA"],
+ },
+ {
+ description: "Input contains in a label element.",
+ document: `<label id="labelB"> label type B
+ <div> inner div
+ <input id="typeB" type="text">
+ </div>
+ </label>`,
+ inputId: "typeB",
+ expectedLabelIds: ["labelB"],
+ },
+ {
+ description: '"for" attribute used to indicate input by one label.',
+ document: `<label id="labelC" for="typeC">label type C</label>
+ <input id="typeC" type="text">`,
+ inputId: "typeC",
+ expectedLabelIds: ["labelC"],
+ },
+ {
+ description: '"for" attribute used to indicate input by multiple labels.',
+ document: `<form>
+ <label id="labelD1" for="typeD">label type D1</label>
+ <label id="labelD2" for="typeD">label type D2</label>
+ <label id="labelD3" for="typeD">label type D3</label>
+ <input id="typeD" type="text">
+ </form>`,
+ inputId: "typeD",
+ expectedLabelIds: ["labelD1", "labelD2", "labelD3"],
+ },
+ {
+ description:
+ '"for" attribute used to indicate input by multiple labels with space prefix/postfix.',
+ document: `<label id="labelE1" for="typeE">label type E1</label>
+ <label id="labelE2" for="typeE ">label type E2</label>
+ <label id="labelE3" for=" TYPEe">label type E3</label>
+ <label id="labelE4" for=" typeE ">label type E4</label>
+ <input id=" typeE " type="text">`,
+ inputId: " typeE ",
+ expectedLabelIds: [],
+ },
+ {
+ description: "Input contains in a label element.",
+ document: `<label id="labelF"> label type F
+ <label for="dummy"> inner label
+ <input id="typeF" type="text">
+ <input id="dummy" type="text">
+ </div>
+ </label>`,
+ inputId: "typeF",
+ expectedLabelIds: ["labelF"],
+ },
+ {
+ description:
+ '"for" attribute used to indicate input by labels out of the form.',
+ document: `<label id="labelG1" for="typeG">label type G1</label>
+ <form>
+ <label id="labelG2" for="typeG">label type G2</label>
+ <input id="typeG" type="text">
+ </form>
+ <label id="labelG3" for="typeG">label type G3</label>`,
+ inputId: "typeG",
+ expectedLabelIds: ["labelG1", "labelG2", "labelG3"],
+ },
+];
+
+TESTCASES.forEach(testcase => {
+ add_task(async function () {
+ info("Starting testcase: " + testcase.description);
+
+ let doc = MockDocument.createTestDocument(
+ "http://localhost:8080/test/",
+ testcase.document
+ );
+
+ let input = doc.getElementById(testcase.inputId);
+ let labels = LabelUtils.findLabelElements(input);
+
+ Assert.deepEqual(
+ labels.map(l => l.id),
+ testcase.expectedLabelIds
+ );
+ LabelUtils.clearLabelMap();
+ });
+});
diff --git a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
new file mode 100644
index 0000000000..63def3d8d9
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -0,0 +1,1300 @@
+/*
+ * Test for form auto fill content helper fill all inputs function.
+ */
+
+"use strict";
+
+var FormAutofillHandler;
+add_task(async function () {
+ ({ FormAutofillHandler } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillHandler.sys.mjs"
+ ));
+});
+
+const DEFAULT_ADDRESS_RECORD = {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+};
+
+const ADDRESS_RECORD_2 = {
+ guid: "address2",
+ "given-name": "John",
+ "additional-name": "Middle",
+ "family-name": "Doe",
+ "postal-code": "940012345",
+};
+
+const DEFAULT_CREDITCARD_RECORD = {
+ guid: "123",
+ "cc-exp-month": 1,
+ "cc-exp-year": 2025,
+ "cc-exp": "2025-01",
+};
+
+const DEFAULT_EXPECTED_CREDITCARD_RECORD = {
+ guid: "123",
+ "cc-exp-month": 1,
+ "cc-exp-year": 2025,
+ "cc-exp": "01/2025",
+};
+
+const getCCExpMonthFormatted = () => {
+ return DEFAULT_CREDITCARD_RECORD["cc-exp-month"].toString().padStart(2, "0");
+};
+
+const getCCExpYearFormatted = () => {
+ return DEFAULT_CREDITCARD_RECORD["cc-exp-year"].toString().substring(2);
+};
+
+// Bug 1767130: If a form has separate inputs for expiry month and year,
+// we will always transform month into MM
+const DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY = {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp-month-formatted": getCCExpMonthFormatted(),
+};
+
+const TESTCASES = [
+ {
+ description: "Address form with street-address",
+ document: `<form>
+ <input autocomplete="given-name">
+ <input autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description: "Address form with street-address, address-line[1, 2, 3]",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ <input id="line3" autocomplete="address-line3">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description: "Address form with street-address, address-line1",
+ document: `<form>
+ <input autocomplete="given-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St line2 line3",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description: "Address form with street-address, address-line[1, 2]",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "Address form with street-address, address-line[1, 3]" +
+ ", determined by autocomplete attr",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line3" autocomplete="address-line3">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ // Since the form is missing address-line2 field, the value of
+ // address-line1 should contain line2 value as well.
+ "address-line1": "2 Harrison St line2",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "Address form with street-address, address-line[1, 3]" +
+ ", determined by heuristics",
+ document: `<form>
+ <input id="street-address">
+ <input id="address-line1">
+ <input id="address-line3">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ // Since the form is missing address-line2 field, the value of
+ // address-line1 should contain line2 value as well.
+ "address-line1": "2 Harrison St line2",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description: "Address form with exact matching options in select",
+ document: `<form>
+ <input autocomplete="given-name">
+ <select autocomplete="address-level1">
+ <option id="option-address-level1-XX" value="XX">Dummy</option>
+ <option id="option-address-level1-CA" value="CA">California</option>
+ </select>
+ <select autocomplete="country">
+ <option id="option-country-XX" value="XX">Dummy</option>
+ <option id="option-country-US" value="US">United States</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ expectedOptionElements: [
+ {
+ "address-level1": "option-address-level1-CA",
+ country: "option-country-US",
+ },
+ ],
+ },
+ {
+ description: "Address form with inexact matching options in select",
+ document: `<form>
+ <input autocomplete="given-name">
+ <select autocomplete="address-level1">
+ <option id="option-address-level1-XX" value="XX">Dummy</option>
+ <option id="option-address-level1-OO" value="OO">California</option>
+ </select>
+ <select autocomplete="country">
+ <option id="option-country-XX" value="XX">Dummy</option>
+ <option id="option-country-OO" value="OO">United States</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ expectedOptionElements: [
+ {
+ "address-level1": "option-address-level1-OO",
+ country: "option-country-OO",
+ },
+ ],
+ },
+ {
+ description: "Address form with value-omitted options in select",
+ document: `<form>
+ <input autocomplete="given-name">
+ <select autocomplete="address-level1">
+ <option id="option-address-level1-1" value="">Dummy</option>
+ <option id="option-address-level1-2" value="">California</option>
+ </select>
+ <select autocomplete="country">
+ <option id="option-country-1" value="">Dummy</option>
+ <option id="option-country-2" value="">United States</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ expectedOptionElements: [
+ {
+ "address-level1": "option-address-level1-2",
+ country: "option-country-2",
+ },
+ ],
+ },
+ {
+ description: "Address form with options with the same value in select ",
+ document: `<form>
+ <input autocomplete="given-name">
+ <select autocomplete="address-level1">
+ <option id="option-address-level1-same1" value="same">Dummy</option>
+ <option id="option-address-level1-same2" value="same">California</option>
+ </select>
+ <select autocomplete="country">
+ <option id="option-country-same1" value="sametoo">Dummy</option>
+ <option id="option-country-same2" value="sametoo">United States</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ expectedOptionElements: [
+ {
+ "address-level1": "option-address-level1-same2",
+ country: "option-country-same2",
+ },
+ ],
+ },
+ {
+ description:
+ "Address form without matching options in select for address-level1 and country",
+ document: `<form>
+ <input autocomplete="given-name">
+ <select autocomplete="address-level1">
+ <option id="option-address-level1-dummy1" value="">Dummy</option>
+ <option id="option-address-level1-dummy2" value="">Dummy 2</option>
+ </select>
+ <select autocomplete="country">
+ <option id="option-country-dummy1" value="">Dummy</option>
+ <option id="option-country-dummy2" value="">Dummy 2</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "Change the tel value of a profile to tel-national for a field without pattern and maxlength.",
+ document: `<form>
+ <input id="telephone">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "9876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ 'Do not change the profile for an autocomplete="tel" field without patern and maxlength.',
+ document: `<form>
+ <input id="tel" autocomplete="tel">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ 'autocomplete="tel" field with `maxlength` can be filled with `tel` value.',
+ document: `<form>
+ <input id="telephone" autocomplete="tel" maxlength="12">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "Still fill `tel-national` in a `tel` field with `maxlength` can be filled with `tel` value.",
+ document: `<form>
+ <input id="telephone" maxlength="12">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "9876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "`tel` field with `maxlength` can be filled with `tel-national` value.",
+ document: `<form>
+ <input id="telephone" maxlength="10">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "9876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "`tel` field with `pattern` attr can be filled with `tel` value.",
+ document: `<form>
+ <input id="telephone" pattern="[+][0-9]+">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "+19876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "Change the tel value of a profile to tel-national one when the pattern is matched.",
+ document: `<form>
+ <input id="telephone" pattern="\d*">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "9876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description: 'Matching pattern when a field is with autocomplete="tel".',
+ document: `<form>
+ <input id="tel" autocomplete="tel" pattern="[0-9]+">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "9876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description:
+ "Checking maxlength of tel field first when a field is with maxlength.",
+ document: `<form>
+ <input id="tel" autocomplete="tel" maxlength="10">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
+ expectedResult: [
+ {
+ guid: "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ "address-level1": "CA",
+ country: "US",
+ tel: "9876543210",
+ "tel-national": "9876543210",
+ },
+ ],
+ },
+ {
+ description: "Address form with maxlength restriction",
+ document: `<form>
+ <input autocomplete="given-name" maxlength="1">
+ <input autocomplete="additional-name" maxlength="1">
+ <input autocomplete="family-name" maxlength="1">
+ <input autocomplete="postal-code" maxlength="5">
+ </form>`,
+ profileData: [{ ...ADDRESS_RECORD_2 }],
+ expectedResult: [
+ {
+ guid: "address2",
+ "given-name": "J",
+ "additional-name": "M",
+ "family-name": "D",
+ "postal-code": "94001",
+ },
+ ],
+ },
+ {
+ description:
+ "Address form with the special cases of the maxlength restriction",
+ document: `<form>
+ <input autocomplete="given-name" maxlength="-1">
+ <input autocomplete="additional-name" maxlength="0">
+ <input autocomplete="family-name" maxlength="1">
+ </form>`,
+ profileData: [{ ...ADDRESS_RECORD_2 }],
+ expectedResult: [
+ {
+ guid: "address2",
+ "given-name": "John",
+ "family-name": "D",
+ "postal-code": "940012345",
+ },
+ ],
+ },
+ {
+ description:
+ "Credit card form with separate fields for expiration month and year",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month">
+ <input autocomplete="cc-exp-year">
+ </form`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [{ ...DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY }],
+ },
+ {
+ description:
+ "Credit Card form with matching options of cc-exp-year and cc-exp-month",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp-month">
+ <option id="option-cc-exp-month-01" value="1">01</option>
+ <option id="option-cc-exp-month-02" value="2">02</option>
+ <option id="option-cc-exp-month-03" value="3">03</option>
+ <option id="option-cc-exp-month-04" value="4">04</option>
+ <option id="option-cc-exp-month-05" value="5">05</option>
+ <option id="option-cc-exp-month-06" value="6">06</option>
+ <option id="option-cc-exp-month-07" value="7">07</option>
+ <option id="option-cc-exp-month-08" value="8">08</option>
+ <option id="option-cc-exp-month-09" value="9">09</option>
+ <option id="option-cc-exp-month-10" value="10">10</option>
+ <option id="option-cc-exp-month-11" value="11">11</option>
+ <option id="option-cc-exp-month-12" value="12">12</option>
+ </select>
+ <select autocomplete="cc-exp-year">
+ <option id="option-cc-exp-year-17" value="2017">17</option>
+ <option id="option-cc-exp-year-18" value="2018">18</option>
+ <option id="option-cc-exp-year-19" value="2019">19</option>
+ <option id="option-cc-exp-year-20" value="2020">20</option>
+ <option id="option-cc-exp-year-21" value="2021">21</option>
+ <option id="option-cc-exp-year-22" value="2022">22</option>
+ <option id="option-cc-exp-year-23" value="2023">23</option>
+ <option id="option-cc-exp-year-24" value="2024">24</option>
+ <option id="option-cc-exp-year-25" value="2025">25</option>
+ <option id="option-cc-exp-year-26" value="2026">26</option>
+ <option id="option-cc-exp-year-27" value="2027">27</option>
+ <option id="option-cc-exp-year-28" value="2028">28</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_CREDITCARD_RECORD],
+ expectedOptionElements: [
+ {
+ "cc-exp-month": "option-cc-exp-month-01",
+ "cc-exp-year": "option-cc-exp-year-25",
+ },
+ ],
+ },
+ {
+ description: "Credit Card form with matching options which contain labels",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp-month">
+ <option value="" selected="selected">Month</option>
+ <option label="01 - January" id="option-cc-exp-month-01" value="object:17">dummy</option>
+ <option label="02 - February" id="option-cc-exp-month-02" value="object:18">dummy</option>
+ <option label="03 - March" id="option-cc-exp-month-03" value="object:19">dummy</option>
+ <option label="04 - April" id="option-cc-exp-month-04" value="object:20">dummy</option>
+ <option label="05 - May" id="option-cc-exp-month-05" value="object:21">dummy</option>
+ <option label="06 - June" id="option-cc-exp-month-06" value="object:22">dummy</option>
+ <option label="07 - July" id="option-cc-exp-month-07" value="object:23">dummy</option>
+ <option label="08 - August" id="option-cc-exp-month-08" value="object:24">dummy</option>
+ <option label="09 - September" id="option-cc-exp-month-09" value="object:25">dummy</option>
+ <option label="10 - October" id="option-cc-exp-month-10" value="object:26">dummy</option>
+ <option label="11 - November" id="option-cc-exp-month-11" value="object:27">dummy</option>
+ <option label="12 - December" id="option-cc-exp-month-12" value="object:28">dummy</option>
+ </select>
+ <select autocomplete="cc-exp-year">
+ <option value="" selected="selected">Year</option>
+ <option label="2017" id="option-cc-exp-year-17" value="object:29">dummy</option>
+ <option label="2018" id="option-cc-exp-year-18" value="object:30">dummy</option>
+ <option label="2019" id="option-cc-exp-year-19" value="object:31">dummy</option>
+ <option label="2020" id="option-cc-exp-year-20" value="object:32">dummy</option>
+ <option label="2021" id="option-cc-exp-year-21" value="object:33">dummy</option>
+ <option label="2022" id="option-cc-exp-year-22" value="object:34">dummy</option>
+ <option label="2023" id="option-cc-exp-year-23" value="object:35">dummy</option>
+ <option label="2024" id="option-cc-exp-year-24" value="object:36">dummy</option>
+ <option label="2025" id="option-cc-exp-year-25" value="object:37">dummy</option>
+ <option label="2026" id="option-cc-exp-year-26" value="object:38">dummy</option>
+ <option label="2027" id="option-cc-exp-year-27" value="object:39">dummy</option>
+ <option label="2028" id="option-cc-exp-year-28" value="object:40">dummy</option>
+ <option label="2029" id="option-cc-exp-year-29" value="object:41">dummy</option>
+ <option label="2030" id="option-cc-exp-year-30" value="object:42">dummy</option>
+ <option label="2031" id="option-cc-exp-year-31" value="object:43">dummy</option>
+ <option label="2032" id="option-cc-exp-year-32" value="object:44">dummy</option>
+ <option label="2033" id="option-cc-exp-year-33" value="object:45">dummy</option>
+ <option label="2034" id="option-cc-exp-year-34" value="object:46">dummy</option>
+ <option label="2035" id="option-cc-exp-year-35" value="object:47">dummy</option>
+ </select>
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_CREDITCARD_RECORD],
+ expectedOptionElements: [
+ {
+ "cc-exp-month": "option-cc-exp-month-01",
+ "cc-exp-year": "option-cc-exp-year-25",
+ },
+ ],
+ },
+ {
+ description: "Compound cc-exp: {MON1}/{YEAR2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="3/17">3/17</option>
+ <option value="1/25" id="selected-cc-exp">1/25</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON1}/{YEAR4}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="3/2017">3/2017</option>
+ <option value="1/2025" id="selected-cc-exp">1/2025</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON2}/{YEAR2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="03/17">03/17</option>
+ <option value="01/25" id="selected-cc-exp">01/25</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON2}/{YEAR4}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="03/2017">03/2017</option>
+ <option value="01/2025" id="selected-cc-exp">01/2025</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON1}-{YEAR2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="3-17">3-17</option>
+ <option value="1-25" id="selected-cc-exp">1-25</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON1}-{YEAR4}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="3-2017">3-2017</option>
+ <option value="1-2025" id="selected-cc-exp">1-2025</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON2}-{YEAR2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="03-17">03-17</option>
+ <option value="01-25" id="selected-cc-exp">01-25</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON2}-{YEAR4}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="03-2017">03-2017</option>
+ <option value="01-2025" id="selected-cc-exp">01-2025</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {YEAR2}-{MON2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="17-03">17-03</option>
+ <option value="25-01" id="selected-cc-exp">25-01</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {YEAR4}-{MON2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="2017-03">2017-03</option>
+ <option value="2025-01" id="selected-cc-exp">2025-01</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {YEAR4}/{MON2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="2017/3">2017/3</option>
+ <option value="2025/1" id="selected-cc-exp">2025/1</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {MON2}{YEAR2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="0317">0317</option>
+ <option value="0125" id="selected-cc-exp">0125</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Compound cc-exp: {YEAR2}{MON2}",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="1703">1703</option>
+ <option value="2501" id="selected-cc-exp">2501</option>
+ </select></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ expectedOptionElements: [{ "cc-exp": "selected-cc-exp" }],
+ },
+ {
+ description: "Fill a cc-exp without cc-exp-month value in the profile",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="03/17">03/17</option>
+ <option value="01/25">01/25</option>
+ </select></form>`,
+ profileData: [
+ {
+ guid: "123",
+ "cc-exp-year": 2025,
+ },
+ ],
+ expectedResult: [
+ {
+ guid: "123",
+ "cc-exp-year": 2025,
+ },
+ ],
+ expectedOptionElements: [],
+ },
+ {
+ description: "Fill a cc-exp without cc-exp-year value in the profile",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp">
+ <option value="03/17">03/17</option>
+ <option value="01/25">01/25</option>
+ </select></form>`,
+ profileData: [
+ {
+ guid: "123",
+ "cc-exp-month": 1,
+ },
+ ],
+ expectedResult: [
+ {
+ guid: "123",
+ "cc-exp-month": 1,
+ },
+ ],
+ expectedOptionElements: [],
+ },
+ {
+ description: "Fill a cc-exp* without cc-exp-month value in the profile",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp-month">
+ <option value="03">03</option>
+ <option value="01">01</option>
+ </select>
+ <select autocomplete="cc-exp-year">
+ <option value="17">2017</option>
+ <option value="25">2025</option>
+ </select>
+ </form>`,
+ profileData: [
+ {
+ guid: "123",
+ "cc-exp-year": 2025,
+ },
+ ],
+ expectedResult: [
+ {
+ guid: "123",
+ "cc-exp-year": 2025,
+ },
+ ],
+ expectedOptionElements: [],
+ },
+ {
+ description: "Fill a cc-exp* without cc-exp-year value in the profile",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <select autocomplete="cc-exp-month">
+ <option value="03">03</option>
+ <option value="01">01</option>
+ </select>
+ <select autocomplete="cc-exp-year">
+ <option value="17">2017</option>
+ <option value="25">2025</option>
+ </select>
+ </form>`,
+ profileData: [
+ {
+ guid: "123",
+ "cc-exp-month": 1,
+ },
+ ],
+ expectedResult: [
+ {
+ guid: "123",
+ "cc-exp-month": 1,
+ },
+ ],
+ expectedOptionElements: [],
+ },
+ {
+ description:
+ "Fill a cc-exp field using adjacent label (MM/YY) as expiry string placeholder",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <label>Expiry (MM/YY)</label>
+ <input autocomplete="cc-exp">
+ </form>
+ `,
+ profileData: [DEFAULT_CREDITCARD_RECORD],
+ expectedResult: [
+ { ...DEFAULT_EXPECTED_CREDITCARD_RECORD, "cc-exp": "01/25" },
+ ],
+ },
+ {
+ description:
+ "Fill a cc-exp field using adjacent label (MM - YY) as expiry string placeholder",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <label>Expiry (MM - YY)</label>
+ <input autocomplete="cc-exp">
+ </form>
+ `,
+ profileData: [DEFAULT_CREDITCARD_RECORD],
+ expectedResult: [
+ { ...DEFAULT_EXPECTED_CREDITCARD_RECORD, "cc-exp": "01-25" },
+ ],
+ },
+ {
+ description: "Fill a cc-exp field correctly while ignoring unrelated label",
+ document: `<form>
+ <label>Credit card number label</label>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp">
+ </form>
+ `,
+ profileData: [DEFAULT_CREDITCARD_RECORD],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ },
+ {
+ description: "Fill a cc-exp without placeholder on the cc-exp field",
+ document: `<form><input autocomplete="cc-number">
+ <input autocomplete="cc-exp"></form>`,
+ profileData: [DEFAULT_CREDITCARD_RECORD],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ },
+ {
+ description:
+ "Fill a cc-exp with whitespace placeholder on the cc-exp field",
+ document: `<form><input autocomplete="cc-number">
+ <input autocomplete="cc-exp" placeholder=" "></form>`,
+ profileData: [DEFAULT_CREDITCARD_RECORD],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm/yy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm/yy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "01/25",
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm / yy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm / yy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "01/25",
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [MM / YY].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="MM / YY" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "01/25",
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm / yyyy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm / yyyy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "01/2025",
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm - yyyy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm - yyyy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "01-2025",
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [yyyy-mm].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="yyyy-mm" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "2025-01",
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mmm yyyy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mmm yyyy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm foo yyyy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm foo yyyy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm - - yyyy].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm - - yyyy" autocomplete="cc-exp"></form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp-month field [mm].",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" placeholder="MM">
+ <input autocomplete="cc-exp-year">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp-month-formatted": getCCExpMonthFormatted(),
+ },
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp-year field [yy].",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month">
+ <input autocomplete="cc-exp-year" placeholder="YY">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY,
+ "cc-exp-year-formatted": getCCExpYearFormatted(),
+ },
+ ],
+ },
+ {
+ description: "Test maxlength=2 on numeric fields.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" maxlength="2">
+ <input autocomplete="cc-exp-year" maxlength="2">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY,
+ "cc-exp-year": 25,
+ },
+ ],
+ },
+ {
+ description: "Test maxlength=4 on numeric fields.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" maxlength="4">
+ <input autocomplete="cc-exp-year" maxlength="4">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY],
+ },
+ // Bug 1687679: The default value of an expiration month, when filled in an input element,
+ // is a two character length string. Because of this, testing a maxlength of 1 is invalid.
+ {
+ description: "Test maxlength=1 on numeric fields.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" maxlength="1">
+ <input autocomplete="cc-exp-year" maxlength="1">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY,
+ "cc-exp-year": 5,
+ "cc-exp-month": 1,
+ },
+ ],
+ },
+ {
+ description: "Test maxlength=0 on numeric fields.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" maxlength="0">
+ <input autocomplete="cc-exp-year" maxlength="0">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ guid: DEFAULT_CREDITCARD_RECORD.guid,
+ "cc-exp": DEFAULT_CREDITCARD_RECORD["cc-exp"],
+ },
+ ],
+ },
+ {
+ // It appears that negative values do not get propagated.
+ description: "Test maxlength=-2 on numeric fields.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" maxlength="-2">
+ <input autocomplete="cc-exp-year" maxlength="-2">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY],
+ },
+ {
+ description: "Test maxlength=10 on numeric fields.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month" maxlength="10">
+ <input autocomplete="cc-exp-year" maxlength="10">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY],
+ },
+ {
+ description: "Test (special case) maxlength=5 on cc-exp field.",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp" maxlength="5">
+ </form>`,
+ profileData: [{ ...DEFAULT_CREDITCARD_RECORD }],
+ expectedResult: [
+ {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp": "01/25",
+ },
+ ],
+ },
+];
+
+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();
+ handler.focusedInput = form.elements[0];
+
+ let adaptedRecords = handler.activeSection.getAdaptedProfiles(
+ testcase.profileData
+ );
+ Assert.deepEqual(adaptedRecords, testcase.expectedResult);
+
+ if (testcase.expectedOptionElements) {
+ testcase.expectedOptionElements.forEach((expectedOptionElement, i) => {
+ for (let field in expectedOptionElement) {
+ let select = form.querySelector(`[autocomplete=${field}]`);
+ let expectedOption = doc.getElementById(expectedOptionElement[field]);
+ Assert.notEqual(expectedOption, null);
+
+ let value = testcase.profileData[i][field];
+ let cache =
+ handler.activeSection._cacheValue.matchingSelectOption.get(select);
+ let targetOption = cache[value] && cache[value].get();
+ Assert.notEqual(targetOption, null);
+
+ Assert.equal(targetOption, expectedOption);
+ }
+ });
+ }
+ });
+}
diff --git a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles_locales.js b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles_locales.js
new file mode 100644
index 0000000000..476559c43d
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles_locales.js
@@ -0,0 +1,272 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test to ensure locale specific placeholders for credit card fields are properly used
+ * to transform various values in the profile.
+ */
+
+"use strict";
+
+const DEFAULT_CREDITCARD_RECORD = {
+ guid: "123",
+ "cc-exp-month": 1,
+ "cc-exp-year": 2025,
+ "cc-exp": "2025-01",
+};
+
+const getCCExpYearFormatted = () => {
+ return DEFAULT_CREDITCARD_RECORD["cc-exp-year"].toString().substring(2);
+};
+
+const getCCExpMonthFormatted = () => {
+ return DEFAULT_CREDITCARD_RECORD["cc-exp-month"].toString().padStart(2, "0");
+};
+
+const DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY_FIELDS = {
+ ...DEFAULT_CREDITCARD_RECORD,
+ "cc-exp-month-formatted": getCCExpMonthFormatted(),
+ "cc-exp-year-formatted": getCCExpYearFormatted(),
+};
+
+const FR_TESTCASES = [
+ {
+ description: "Use placeholder to adjust cc-exp format [mm/aa].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm/aa" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm / aa].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm / aa" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [MM / AA].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="MM / AA" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm / aaaa].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm / aaaa" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/2025",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm - aaaa].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm - aaaa" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01-2025",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [aaaa-mm].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="aaaa-mm" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "2025-01",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp-year field [aa].",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month">
+ <input autocomplete="cc-exp-year" placeholder="AA">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ { ...DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY_FIELDS },
+ ],
+ },
+];
+
+const DE_TESTCASES = [
+ {
+ description: "Use placeholder to adjust cc-exp format [mm / jj].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm / jj" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [MM / JJ].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="MM / JJ" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm / jjjj].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm / jjjj" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/2025",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [MM / JJJJ].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="MM / JJJJ" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01/2025",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm - jj].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm - jj" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01-25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [MM - JJ].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="MM - JJ" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01-25",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [mm - jjjj].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="mm - jjjj" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01-2025",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [MM - JJJJ].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="MM - JJJJ" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "01-2025",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp format [jjjj - mm].",
+ document: `<form><input autocomplete="cc-number">
+ <input placeholder="jjjj - mm" autocomplete="cc-exp"></form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
+ "cc-exp": "2025-01",
+ }),
+ ],
+ },
+ {
+ description: "Use placeholder to adjust cc-exp-year field [jj].",
+ document: `<form>
+ <input autocomplete="cc-number">
+ <input autocomplete="cc-exp-month">
+ <input autocomplete="cc-exp-year" placeholder="JJ">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
+ expectedResult: [
+ { ...DEFAULT_EXPECTED_CREDITCARD_RECORD_SEPARATE_EXPIRY_FIELDS },
+ ],
+ },
+];
+
+const TESTCASES = [FR_TESTCASES, DE_TESTCASES];
+
+for (let localeTests of TESTCASES) {
+ for (let testcase of localeTests) {
+ 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();
+ handler.focusedInput = form.elements[0];
+ let adaptedRecords = handler.activeSection.getAdaptedProfiles(
+ testcase.profileData
+ );
+ Assert.deepEqual(adaptedRecords, testcase.expectedResult);
+
+ if (testcase.expectedOptionElements) {
+ testcase.expectedOptionElements.forEach((expectedOptionElement, i) => {
+ for (let field in expectedOptionElement) {
+ let select = form.querySelector(`[autocomplete=${field}]`);
+ let expectedOption = doc.getElementById(
+ expectedOptionElement[field]
+ );
+ Assert.notEqual(expectedOption, null);
+
+ let value = testcase.profileData[i][field];
+ let cache =
+ handler.activeSection._cacheValue.matchingSelectOption.get(
+ select
+ );
+ let targetOption = cache[value] && cache[value].get();
+ Assert.notEqual(targetOption, null);
+
+ Assert.equal(targetOption, expectedOption);
+ }
+ });
+ }
+ });
+ }
+}
diff --git a/browser/extensions/formautofill/test/unit/test_getCategoriesFromFieldNames.js b/browser/extensions/formautofill/test/unit/test_getCategoriesFromFieldNames.js
new file mode 100644
index 0000000000..66f4c18ea9
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getCategoriesFromFieldNames.js
@@ -0,0 +1,95 @@
+"use strict";
+
+var FormAutofillUtils;
+add_task(async function () {
+ ({ FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+ ));
+});
+
+add_task(async function test_isAddressField_isCreditCardField() {
+ const TEST_CASES = {
+ "given-name": {
+ isAddressField: true,
+ isCreditCardField: false,
+ },
+ organization: {
+ isAddressField: true,
+ isCreditCardField: false,
+ },
+ "address-line2": {
+ isAddressField: true,
+ isCreditCardField: false,
+ },
+ tel: {
+ isAddressField: true,
+ isCreditCardField: false,
+ },
+ email: {
+ isAddressField: true,
+ isCreditCardField: false,
+ },
+ "cc-number": {
+ isAddressField: false,
+ isCreditCardField: true,
+ },
+ UNKNOWN: {
+ isAddressField: false,
+ isCreditCardField: false,
+ },
+ "": {
+ isAddressField: false,
+ isCreditCardField: false,
+ },
+ };
+
+ for (let fieldName of Object.keys(TEST_CASES)) {
+ info("Starting testcase: " + fieldName);
+ let field = TEST_CASES[fieldName];
+ Assert.equal(
+ FormAutofillUtils.isAddressField(fieldName),
+ field.isAddressField,
+ "isAddressField"
+ );
+ Assert.equal(
+ FormAutofillUtils.isCreditCardField(fieldName),
+ field.isCreditCardField,
+ "isCreditCardField"
+ );
+ }
+});
+
+add_task(async function test_getCategoriesFromFieldNames() {
+ const TEST_CASES = [
+ {
+ fieldNames: ["given-name", "family-name", "name", "tel", "organization"],
+ set: ["name", "tel", "organization"],
+ },
+ {
+ fieldNames: [
+ "address-line2",
+ "family-name",
+ "name",
+ "tel",
+ "organization",
+ "email",
+ ],
+ set: ["address", "name", "tel", "organization", "email"],
+ },
+ {
+ fieldNames: ["address-line2", "family-name", "", "name", "tel", "UNKOWN"],
+ set: ["address", "name", "tel"],
+ },
+ {
+ fieldNames: ["tel", "family-name", "", "name", "tel", "UNKOWN"],
+ set: ["tel", "name"],
+ },
+ ];
+
+ for (let tc of TEST_CASES) {
+ let categories = FormAutofillUtils.getCategoriesFromFieldNames(
+ tc.fieldNames
+ );
+ Assert.deepEqual(Array.from(categories), tc.set);
+ }
+});
diff --git a/browser/extensions/formautofill/test/unit/test_getCreditCardLogo.js b/browser/extensions/formautofill/test/unit/test_getCreditCardLogo.js
new file mode 100644
index 0000000000..e740d6102a
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getCreditCardLogo.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_getCreditCardLogo() {
+ const { CreditCard } = ChromeUtils.importESModule(
+ "resource://gre/modules/CreditCard.sys.mjs"
+ );
+ // Credit card logos can be either PNG or SVG
+ // so we construct an array that includes both of these file extensions
+ // and test to see if the logo from getCreditCardLogo matches.
+ for (let network of CreditCard.getSupportedNetworks()) {
+ const PATH_PREFIX = "chrome://formautofill/content/third-party/cc-logo-";
+ let actual = CreditCard.getCreditCardLogo(network);
+ Assert.ok(
+ [".png", ".svg"].map(x => PATH_PREFIX + network + x).includes(actual)
+ );
+ }
+ let genericLogo = CreditCard.getCreditCardLogo("null");
+ Assert.equal(
+ genericLogo,
+ "chrome://formautofill/content/icon-credit-card-generic.svg"
+ );
+});
diff --git a/browser/extensions/formautofill/test/unit/test_getFormInputDetails.js b/browser/extensions/formautofill/test/unit/test_getFormInputDetails.js
new file mode 100644
index 0000000000..0ca9def464
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getFormInputDetails.js
@@ -0,0 +1,204 @@
+"use strict";
+
+var FormAutofillContent;
+add_task(async function () {
+ ({ FormAutofillContent } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillContent.sys.mjs"
+ ));
+});
+
+const TESTCASES = [
+ {
+ description: "Form containing 5 fields with autocomplete attribute.",
+ document: `<form id="form1">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <select id="country" autocomplete="country"></select>
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ </form>`,
+ targetInput: ["street-addr", "email"],
+ expectedResult: [
+ {
+ input: {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ formId: "form1",
+ form: [
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "address-level2",
+ },
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "country",
+ },
+ { section: "", addressType: "", contactType: "", fieldName: "email" },
+ { section: "", addressType: "", contactType: "", fieldName: "tel" },
+ ],
+ },
+ {
+ input: {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "email",
+ },
+ formId: "form1",
+ form: [
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "address-level2",
+ },
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "country",
+ },
+ { section: "", addressType: "", contactType: "", fieldName: "email" },
+ { section: "", addressType: "", contactType: "", fieldName: "tel" },
+ ],
+ },
+ ],
+ },
+ {
+ description: "2 forms that are able to be auto filled",
+ document: `<form id="form2">
+ <input id="home-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <select id="country" autocomplete="country"></select>
+ </form>
+ <form id="form3">
+ <input id="office-addr" autocomplete="street-address">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ </form>`,
+ targetInput: ["home-addr", "office-addr"],
+ expectedResult: [
+ {
+ input: {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ formId: "form2",
+ form: [
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "address-level2",
+ },
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "country",
+ },
+ ],
+ },
+ {
+ input: {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ formId: "form3",
+ form: [
+ {
+ section: "",
+ addressType: "",
+ contactType: "",
+ fieldName: "street-address",
+ },
+ { section: "", addressType: "", contactType: "", fieldName: "email" },
+ { section: "", addressType: "", contactType: "", fieldName: "tel" },
+ ],
+ },
+ ],
+ },
+];
+
+function inputDetailAssertion(detail, expected) {
+ Assert.equal(detail.section, expected.section);
+ Assert.equal(detail.addressType, expected.addressType);
+ Assert.equal(detail.contactType, expected.contactType);
+ Assert.equal(detail.fieldName, expected.fieldName);
+ Assert.equal(detail.elementWeakRef.get(), expected.elementWeakRef.get());
+}
+
+TESTCASES.forEach(testcase => {
+ add_task(async function () {
+ info("Starting testcase: " + testcase.description);
+
+ let doc = MockDocument.createTestDocument(
+ "http://localhost:8080/test/",
+ testcase.document
+ );
+
+ for (let i in testcase.targetInput) {
+ let input = doc.getElementById(testcase.targetInput[i]);
+ FormAutofillContent.identifyAutofillFields(input);
+ FormAutofillContent.updateActiveInput(input);
+
+ // Put the input element reference to `element` to make sure the result of
+ // `activeFieldDetail` contains the same input element.
+ testcase.expectedResult[i].input.elementWeakRef =
+ Cu.getWeakReference(input);
+
+ inputDetailAssertion(
+ FormAutofillContent.activeFieldDetail,
+ testcase.expectedResult[i].input
+ );
+
+ let formDetails = testcase.expectedResult[i].form;
+ for (let formDetail of formDetails) {
+ // Compose a query string to get the exact reference of <input>/<select>
+ // element, e.g. #form1 > *[autocomplete="street-address"]
+ let queryString =
+ "#" +
+ testcase.expectedResult[i].formId +
+ " > *[autocomplete=" +
+ formDetail.fieldName +
+ "]";
+ formDetail.elementWeakRef = Cu.getWeakReference(
+ doc.querySelector(queryString)
+ );
+ }
+
+ FormAutofillContent.activeFormDetails.forEach((detail, index) => {
+ inputDetailAssertion(detail, formDetails[index]);
+ });
+ }
+ });
+});
diff --git a/browser/extensions/formautofill/test/unit/test_getInfo.js b/browser/extensions/formautofill/test/unit/test_getInfo.js
new file mode 100644
index 0000000000..802fcd79e9
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getInfo.js
@@ -0,0 +1,363 @@
+"use strict";
+
+var { FormAutofillHeuristics } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs"
+);
+var { LabelUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/LabelUtils.sys.mjs"
+);
+var { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+
+const TESTCASES = [
+ {
+ description: "Input element in a label element",
+ document: `<form>
+ <label> E-Mail
+ <input id="targetElement" type="text">
+ </label>
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description:
+ "A label element is out of the form contains the related input",
+ document: `<label for="targetElement"> E-Mail</label>
+ <form>
+ <input id="targetElement" type="text">
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description: "A label element contains span element",
+ document: `<label for="targetElement">FOO<span>E-Mail</span>BAR</label>
+ <form>
+ <input id="targetElement" type="text">
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description: "The signature in 'name' attr of an input",
+ document: `<input id="targetElement" name="email" type="text">`,
+ elementId: "targetElement",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description: "The signature in 'id' attr of an input",
+ document: `<input id="targetElement_email" name="tel" type="text">`,
+ elementId: "targetElement_email",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description: "Select element in a label element",
+ document: `<form>
+ <label> State
+ <select id="targetElement"></select>
+ </label>
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["address-level1", null, null],
+ },
+ {
+ description: "A select element without a form wrapped",
+ document: `<label for="targetElement">State</label>
+ <select id="targetElement"></select>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["address-level1", null, null],
+ },
+ {
+ description: "address line input",
+ document: `<label for="targetElement">street</label>
+ <input id="targetElement" type="text">`,
+ elementId: "targetElement",
+ expectedReturnValue: ["street-address", null, null],
+ },
+ {
+ description: "CJK character - Traditional Chinese",
+ document: `<label> 郵遞區號
+ <input id="targetElement" />
+ </label>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["postal-code", null, null],
+ },
+ {
+ description: "CJK character - Japanese",
+ document: `<label> 郵便番号
+ <input id="targetElement" />
+ </label>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["postal-code", null, null],
+ },
+ {
+ description: "CJK character - Korean",
+ document: `<label> 우편 번호
+ <input id="targetElement" />
+ </label>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["postal-code", null, null],
+ },
+ {
+ description: "",
+ document: `<input id="targetElement" name="fullname">`,
+ elementId: "targetElement",
+ expectedReturnValue: ["name", null, null],
+ },
+ {
+ description: 'input element with "submit" type',
+ document: `<input id="targetElement" type="submit" />`,
+ elementId: "targetElement",
+ expectedReturnValue: [null, null, null],
+ },
+ {
+ description: "The signature in 'name' attr of an email input",
+ document: `<input id="targetElement" name="email" type="number">`,
+ elementId: "targetElement",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description: 'input element with "email" type',
+ document: `<input id="targetElement" type="email" />`,
+ elementId: "targetElement",
+ expectedReturnValue: ["email", null, null],
+ },
+ {
+ description: "Exclude United State string",
+ document: `<label>United State
+ <input id="targetElement" />
+ </label>`,
+ elementId: "targetElement",
+ expectedReturnValue: [null, null, null],
+ },
+ {
+ description: '"County" field with "United State" string',
+ document: `<label>United State County
+ <input id="targetElement" />
+ </label>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["address-level1", null, null],
+ },
+ {
+ description: '"city" field with double "United State" string',
+ document: `<label>United State united sTATE city
+ <input id="targetElement" />
+ </label>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["address-level2", null, null],
+ },
+ {
+ description: "Verify credit card number",
+ document: `<form>
+ <label for="targetElement"> Card Number</label>
+ <input id="targetElement" type="text">
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["cc-number", null, 1],
+ },
+ {
+ description: "Identify credit card type field",
+ document: `<form>
+ <label for="targetElement">Card Type</label>
+ <input id="targetElement" type="text">
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["cc-type", null, null],
+ },
+ {
+ description: `Identify address field when contained in a form with autocomplete="off"`,
+ document: `<form autocomplete="off">
+ <input id="given-name">
+ </form>`,
+ elementId: "given-name",
+ expectedReturnValue: ["given-name", null, null],
+ },
+ {
+ description: `Identify address field that has a placeholder but no label associated with it`,
+ document: `<form>
+ <input id="targetElement" placeholder="Name">
+ </form>`,
+ elementId: "targetElement",
+ expectedReturnValue: ["name", null, null],
+ },
+ {
+ description: `Identify address field that has a placeholder, no associated label, and its autocomplete attribute is "off"`,
+ document: `<form>
+ <input id="targetElement" placeholder="Address" autocomplete="off">
+ </form>`,
+ 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: `<form autocomplete="off">
+ <input id="targetElement" placeholder="Country">
+ </form>`,
+ 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: `<select id="${label}"></select>`,
+ 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 = `<form autocomplete="off">
+ <label for="targetElement"> Card Number</label>
+ <input id="targetElement" type="text">
+ </form>`;
+ 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 = `<form>
+ <label for="targetElement"> Card Number</label>
+ <input id="targetElement" type="text">
+ </form>`;
+ 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 = `<form autocomplete="off">
+ <input id="given-name">
+ </form>`;
+ 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 = `<form>
+ <input id="given-name">
+ </form>`;
+ 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: `<input id="targetElement" type="text">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" type="email">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" type="number">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" type="tel">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" type="radio">`,
+ fieldId: "targetElement",
+ expectedResult: false,
+ },
+ {
+ document: `<input id="targetElement" type="text" autocomplete="off">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" type="unknown">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" value="JOHN DOE">`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<select id="targetElement" autocomplete="off"></select>`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<select id="targetElement"></select>`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<select id="targetElement" multiple></select>`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<div id="targetElement"></div>`,
+ fieldId: "targetElement",
+ expectedResult: false,
+ },
+ {
+ document: `<input id="targetElement" type="text" readonly>`,
+ fieldId: "targetElement",
+ expectedResult: true,
+ },
+ {
+ document: `<input id="targetElement" type="text" disabled>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="additional-name" autocomplete="additional-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="without-autocomplete-1">
+ <input id="without-autocomplete-2">
+ </form>`,
+ 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: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="without-autocomplete-1">
+ <input id="without-autocomplete-2">
+ </form>`,
+ targetElementId: "street-addr",
+ expectedResult: [],
+ },
+ {
+ description: "Fields without form element.",
+ document: `<input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="without-autocomplete-1">
+ <input id="without-autocomplete-2">`,
+ targetElementId: "street-addr",
+ expectedResult: ["street-addr", "city", "country", "email", "tel"],
+ },
+ {
+ description: "Form containing credit card autocomplete attributes.",
+ document: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-name">
+ <input id="cc-exp-month">
+ <input id="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-number5" maxlength="4">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-name">
+ <input id="cc-exp-month">
+ <input id="cc-exp-year">
+ </form>`,
+ 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: `<form>
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-number5" maxlength="4">
+ <input id="cc-name">
+ <input id="cc-exp-month">
+ <input id="cc-exp-year">
+ </form>`,
+ 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 = `<form id="form1">
+ <input id="street-addr" autocomplete="street-address">
+ <select id="address-level1" autocomplete="address-level1">
+ <option value=""></option>
+ <option value="AL">Alabama</option>
+ <option value="AK">Alaska</option>
+ <option value="AP">Armed Forces Pacific</option>
+
+ <option value="ca">california</option>
+ <option value="AR">US-Arkansas</option>
+ <option value="US-CA">California</option>
+ <option value="CA">California</option>
+ <option value="US-AZ">US_Arizona</option>
+ <option value="Ariz">Arizonac</option>
+ </select>
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ <select id="cc-type">
+ <option value="">Select</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">Master Card</option>
+ <option value="amex">American Express</option>
+ </select>
+ <input id="submit" type="submit">
+ </form>`;
+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: `<form id="form1">
+ <input id="cc-type" autocomplete="cc-type">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ <input id="submit" type="submit">
+ </form>
+ `,
+ 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: `<form id="form1">
+ <input id="cc-type" autocomplete="cc-type">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-number4" maxlength="4">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ <input id="submit" type="submit">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2" readonly value="TEST CITY">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="country" autocomplete="country" disabled value="US">
+ </form>`,
+ 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: `<form>
+ <input id="given-name" autocomplete="shipping given-name">
+ <select id="country" autocomplete="shipping country">
+ <option value=""></option>
+ <option value="US">United States</option>
+ </select>
+ <select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">California</option>
+ <option value="WA">Washington</option>
+ </select>
+ </form>`,
+ 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: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ <input id="cc-csc" autocomplete="cc-csc">
+ </form>
+ `,
+ 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