Bug 1847687 - Enable checking a credit card or address field's focusability by calling Services.focus.elementIsFocusable before autofilling - r=credential-management-reviewers,dimi

Differential Revision: https://phabricator.services.mozilla.com/D189072
This commit is contained in:
jneuberger 2023-10-12 09:13:36 +00:00
Родитель f1a05364e3
Коммит ae6038dcc3
10 изменённых файлов: 314 добавлений и 66 удалений

Просмотреть файл

@ -22,6 +22,8 @@ skip-if = ["apple_silicon && !debug"]
["browser_ignore_invisible_fields.js"]
["browser_ignore_unfocusable_fields.js"]
["browser_label_rules.js"]
["browser_multiple_section.js"]

Просмотреть файл

@ -7,7 +7,14 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
add_heuristic_tests([
{
description: "all fields are visible",
description:
"All fields are visible (interactivityCheckMode is set to visibility).",
prefs: [
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"visibility",
],
],
fixtureData: `
<html>
<body>
@ -44,7 +51,14 @@ add_heuristic_tests([
],
},
{
description: "some fields are invisible because of css style",
description:
"Some fields are invisible due to css styling (interactivityCheckMode is set to visibility).",
prefs: [
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"visibility",
],
],
fixtureData: `
<html>
<body>
@ -52,9 +66,9 @@ add_heuristic_tests([
<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" />
<input type="text" id="country" autocomplete="country" />
<input type="text" id="postal-code" autocomplete="postal-code" hidden />
<input type="text" id="address-line1" autocomplete="address-line1" style="display:none" />
<div style="visibility: hidden">
<input type="text" id="address-line2" autocomplete="address-line2" />
</div>
@ -71,15 +85,22 @@ add_heuristic_tests([
{ fieldName: "name" },
{ fieldName: "tel" },
{ fieldName: "email" },
{ fieldName: "country" },
],
},
],
},
{
// hidden and style="display:none" are always considered regardless what visibility check we use
// hidden, style="display:none" are always considered (when mode visibility)
description:
"invisible fields are identified because number of elemenent in the form exceed the threshold",
prefs: [["extensions.formautofill.heuristics.visibilityCheckThreshold", 1]],
"Number of form elements exceeds the threshold (interactivityCheckMode is set to visibility).",
prefs: [
["extensions.formautofill.heuristics.visibilityCheckThreshold", 1],
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"visibility",
],
],
fixtureData: `
<html>
<body>
@ -87,9 +108,9 @@ add_heuristic_tests([
<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" />
<input type="text" id="country" autocomplete="country" disabled />
<input type="text" id="postal-code" autocomplete="postal-code" hidden />
<input type="text" id="address-line1" autocomplete="address-line1" style="display:none" />
<div style="visibility: hidden">
<input type="text" id="address-line2" autocomplete="address-line2" />
</div>
@ -106,7 +127,7 @@ add_heuristic_tests([
{ fieldName: "name" },
{ fieldName: "tel" },
{ fieldName: "email" },
{ fieldName: "address-line1" },
{ fieldName: "country" },
{ fieldName: "address-line2" },
],
},

Просмотреть файл

@ -0,0 +1,179 @@
/* 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 visual fields are considered focusable.",
prefs: [
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"focusability",
],
],
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"/>
<select id="country" autocomplete="country">
<option value="United States">United States</option>
</select>
<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" },
],
},
],
},
{
// ignore opacity (see Bug 1835852),
description:
"Invisible fields with style.opacity=0 set are considered focusable.",
prefs: [
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"focusability",
],
],
fixtureData: `
<html>
<body>
<form>
<input type="text" id="name" autocomplete="name" style="opacity:0" />
<input type="text" id="tel" autocomplete="tel" />
<input type="text" id="email" autocomplete="email" style="opacity:0"/>
<select id="country" autocomplete="country">
<option value="United States">United States</option>
</select>
<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 considered unfocusable due to their invisibility.",
prefs: [
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"focusability",
],
],
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" hidden />
<input type="text" id="address-line1" autocomplete="address-line1" style="display:none" />
<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: "country" },
],
},
],
},
{
description: `Disabled field and field with tabindex="-1" is considered unfocusable`,
prefs: [
[
"extensions.formautofill.heuristics.interactivityCheckMode",
"focusability",
],
],
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" disabled/>
<input type="text" id="postal-code" autocomplete="postal-code" tabindex="-1"/>
<input type="text" id="address-line1" autocomplete="address-line1" />
<input type="text" id="address-line2" autocomplete="address-line2" />
</form>
</body>
</html>
`,
expectedResult: [
{
default: {
reason: "autocomplete",
},
fields: [
{ fieldName: "name" },
{ fieldName: "tel" },
{ fieldName: "email" },
{ fieldName: "address-line1" },
{ fieldName: "address-line2" },
],
},
],
},
]);

Просмотреть файл

@ -71,6 +71,24 @@ region-name-tw = Taiwan
L10nRegistry.getInstance().registerSources([mockSource]);
}
/**
* Mock the return value of Services.focus.elementIsFocusable
* since a field's focusability can't be tested in a unit test.
*/
(function ignoreAFieldsFocusability() {
let stub = sinon.stub(Services, "focus").get(() => {
return {
elementIsFocusable() {
return true;
},
};
});
registerCleanupFunction(() => {
stub.restore();
});
})();
do_get_profile();
const EXTENSION_ID = "formautofill@mozilla.org";

Просмотреть файл

@ -421,39 +421,6 @@ const TESTCASES = [
"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",

Просмотреть файл

@ -3986,6 +3986,9 @@ pref("extensions.formautofill.creditCards.heuristics.fathom.testConfidence", "0"
pref("extensions.formautofill.firstTimeUse", true);
pref("extensions.formautofill.loglevel", "Warn");
// The interactivityCheckMode pref is only temporary.
// It will be removed when we decide to only support the `focusability` mode
pref("extensions.formautofill.heuristics.interactivityCheckMode", "focusability");
pref("toolkit.osKeyStore.loglevel", "Warn");

Просмотреть файл

@ -27,9 +27,10 @@ const IOS_DEFAULT_PREFERENCES = {
"extensions.formautofill.addresses.ignoreAutocompleteOff": true,
"extensions.formautofill.heuristics.enabled": true,
"extensions.formautofill.section.enabled": true,
// WebKit doesn't support the checkVisibility API, setting the threshold value to 0 to esnure
// WebKit doesn't support the checkVisibility API, setting the threshold value to 0 to ensure
// `IsFieldVisible` function doesn't use it
"extensions.formautofill.heuristics.visibilityCheckThreshold": 0,
"extensions.formautofill.heuristics.interactivityCheckMode": "focusability",
"extensions.formautofill.focusOnAutofill": false,
};

Просмотреть файл

@ -123,10 +123,23 @@ export const OSKeyStore = withNotImplementedError({
ensureLoggedIn: () => true,
});
// Checks an element's focusability and accessibility via keyboard navigation
const checkFocusability = element => {
return (
!element.disabled &&
!element.hidden &&
element.style.display != "none" &&
element.tabIndex != "-1"
);
};
// Define mock for Services
// NOTE: Services is a global so we need to attach it to the window
// eslint-disable-next-line no-shadow
export const Services = withNotImplementedError({
focus: withNotImplementedError({
elementIsFocusable: checkFocusability,
}),
intl: withNotImplementedError({
getAvailableLocaleDisplayNames: () => [],
getRegionDisplayNames: () => [],

Просмотреть файл

@ -550,25 +550,7 @@ export const FormAutofillHeuristics = {
* all sections within its field details in the form.
*/
getFormInfo(form) {
let elements = Array.from(form.elements).filter(element =>
lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)
);
// Due to potential performance impact while running visibility check on
// a large amount of elements, a comprehensive visibility check
// (considering opacity and CSS visibility) is only applied when the number
// of eligible elements is below a certain threshold.
const runVisiblityCheck =
elements.length < lazy.FormAutofillUtils.visibilityCheckThreshold;
if (!runVisiblityCheck) {
lazy.log.debug(
`Skip running visibility check, because of too many elements (${elements.length})`
);
}
elements = elements.filter(element =>
lazy.FormAutofillUtils.isFieldVisible(element, runVisiblityCheck)
);
let elements = this.getFormElements(form);
const scanner = new lazy.FieldScanner(elements, element =>
this.inferFieldInfo(element, elements)
@ -617,6 +599,44 @@ export const FormAutofillHeuristics = {
);
},
/**
* Get form elements that are of credit card or address type and filtered by either
* visibility or focusability - depending on the interactivity mode (default = focusability)
* This distinction is only temporary as we want to test switching from visibility mode
* to focusability mode. The visibility mode is then removed.
*
* @param {HTMLElement} form
* @returns {Array<HTMLElement>} elements filtered by interactivity mode (visibility or focusability)
*/
getFormElements(form) {
let elements = Array.from(form.elements).filter(element =>
lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)
);
const interactivityMode = lazy.FormAutofillUtils.interactivityCheckMode;
if (interactivityMode == "focusability") {
elements = elements.filter(element =>
lazy.FormAutofillUtils.isFieldFocusable(element)
);
} else if (interactivityMode == "visibility") {
// Due to potential performance impact while running visibility check on
// a large amount of elements, a comprehensive visibility check
// (considering opacity and CSS visibility) is only applied when the number
// of eligible elements is below a certain threshold.
const runVisiblityCheck =
elements.length < lazy.FormAutofillUtils.visibilityCheckThreshold;
if (!runVisiblityCheck) {
lazy.log.debug(
`Skip running visibility check, because of too many elements (${elements.length})`
);
}
elements = elements.filter(element =>
lazy.FormAutofillUtils.isFieldVisible(element, runVisiblityCheck)
);
}
return elements;
},
/**
* The result is an array contains the sections with its belonging field details.
*

Просмотреть файл

@ -438,7 +438,7 @@ FormAutofillUtils = {
* @returns {boolean} true if the element is visible
*/
isFieldVisible(element, visibilityCheck = true) {
if (visibilityCheck) {
if (visibilityCheck && element.checkVisibility) {
return element.checkVisibility({
checkOpacity: true,
checkVisibilityCSS: true,
@ -448,6 +448,23 @@ FormAutofillUtils = {
return !element.hidden && element.style.display != "none";
},
/**
* Determines if an element is focusable
* and accessible via keyboard navigation or not.
*
* @param {HTMLElement} element
*
* @returns {bool} true if the element is focusable and accessible
*/
isFieldFocusable(element) {
return (
// The Services.focus.elementIsFocusable API considers elements with
// tabIndex="-1" set as focusable. But since they are not accessible
// via keyboard navigation we treat them as non-interactive
Services.focus.elementIsFocusable(element, 0) && element.tabIndex != "-1"
);
},
/**
* Determines if an element is eligible to be used by credit card or address autofill.
*
@ -1226,6 +1243,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
200
);
XPCOMUtils.defineLazyPreferenceGetter(
FormAutofillUtils,
"interactivityCheckMode",
"extensions.formautofill.heuristics.interactivityCheckMode",
"focusability"
);
// This is only used in iOS
XPCOMUtils.defineLazyPreferenceGetter(
FormAutofillUtils,