Bug 1662879 part 2: Expose the credit card type via aria-label for credit card autocomplete results. r=zbraniecki
authorJames Teh <jteh@mozilla.com>
Fri, 11 Sep 2020 05:50:25 +0000
changeset 548270 9845c9d150a0e3274f228bbb8c9c3e8d936e6a42
parent 548269 928ddc1ceffb08c3634eac83d04895032eca12bf
child 548271 3010f73c9eec545c42715659f3593516ef686f70
push id37775
push usermalexandru@mozilla.com
push dateFri, 11 Sep 2020 09:30:56 +0000
treeherdermozilla-central@361808562220 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerszbraniecki
bugs1662879
milestone82.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1662879 part 2: Expose the credit card type via aria-label for credit card autocomplete results. r=zbraniecki Differential Revision: https://phabricator.services.mozilla.com/D89549
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/content/customElements.js
browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js
browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html
browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -416,24 +416,40 @@ class CreditCardResult extends ProfileAu
         let primaryAffix;
         let primary = profile[focusedFieldName];
 
         if (focusedFieldName == "cc-number") {
           let { affix, label } = CreditCard.formatMaskedNumber(primary);
           primaryAffix = affix;
           primary = label;
         }
+        const secondary = this._getSecondaryLabel(
+          focusedFieldName,
+          allFieldNames,
+          profile
+        );
+        // 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.
+        let ccTypeName;
+        try {
+          ccTypeName = FormAutofillUtils.stringBundle.GetStringFromName(
+            `cardNetwork.${profile["cc-type"]}`
+          );
+        } catch (e) {
+          ccTypeName = null; // Unknown.
+        }
+        const ariaLabel = [ccTypeName, primaryAffix, primary, secondary]
+          .filter(chunk => !!chunk) // Exclude empty chunks.
+          .join(" ");
         return {
           primaryAffix,
           primary,
-          secondary: this._getSecondaryLabel(
-            focusedFieldName,
-            allFieldNames,
-            profile
-          ),
+          secondary,
+          ariaLabel,
         };
       });
     // Add an empty result entry for footer.
     labels.push({ primary: "", secondary: "" });
 
     return labels;
   }
 
--- a/browser/extensions/formautofill/content/customElements.js
+++ b/browser/extensions/formautofill/content/customElements.js
@@ -138,23 +138,26 @@
     _adjustAcItem() {
       this._adjustAutofillItemLayout();
       this.setAttribute("formautofillattached", "true");
       this._itemBox.style.setProperty(
         "--primary-icon",
         `url(${this.getAttribute("ac-image")})`
       );
 
-      let { primaryAffix, primary, secondary } = JSON.parse(
+      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" }
   );
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js
@@ -28,16 +28,20 @@ add_task(async function test_credit_card
   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"
     );
--- 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
@@ -69,56 +69,61 @@ add_task(async function all_saved_fields
   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);
@@ -160,16 +165,17 @@ add_task(async function check_fields_aft
   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;
 });
@@ -192,16 +198,17 @@ add_task(async function check_cc_popup_o
     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;
   }
@@ -210,16 +217,17 @@ add_task(async function check_form_autof
   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">
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
@@ -61,24 +61,26 @@ add_task(async function check_menu_when_
 
   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">
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -454,9 +454,17 @@ function checkUsagePrefs(hasEntry, lastU
     0
   );
   ok(
     lastUsed - lastUsedPref < 10,
     `lastUsed usage pref (${lastUsedPref}) is within 10 seconds of ${lastUsed}`
   );
 }
 
+/*
+ * 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();
--- a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
+++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
@@ -287,26 +287,28 @@ let creditCardTestCases = [
       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",
@@ -322,38 +324,41 @@ let creditCardTestCases = [
         {
           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",