[PM-10420] Autofill focus jumps around after autofilling identity (#10361)

* [PM-10420] Autofill focus jumps around after autofilling identity ciphers

* [PM-10420] Autofill focus jumps around after autofilling identity ciphers

* [PM-10420] Autofill focus jumps around after autofilling identity ciphers

* [PM-10420] Incorporating the feature flag within jest to test the validity of both implementations

* [PM-10420] Refactoring how we compile the combined list of keywords

* [PM-10420] Adding JSDocs to the implemented methods
This commit is contained in:
Cesar Gonzalez 2024-08-02 14:14:23 -05:00 committed by GitHub
parent c50a9063bc
commit 76351ce750
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 977 additions and 420 deletions

View File

@ -76,6 +76,11 @@ export class AutoFillConstants {
"textarea",
...AutoFillConstants.ExcludedAutofillTypes,
];
static readonly ExcludedIdentityAutocompleteTypes: Set<string> = new Set([
"current-password",
"new-password",
]);
}
export class CreditCardAutoFillConstants {

View File

@ -60,7 +60,7 @@ import {
GenerateFillScriptOptions,
PageDetail,
} from "./abstractions/autofill.service";
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
import { AutoFillConstants } from "./autofill-constants";
import AutofillService from "./autofill.service";
const mockEquivalentDomains = [
@ -3056,12 +3056,12 @@ describe("AutofillService", () => {
options.cipher.identity = mock<IdentityView>();
});
it("returns null if an identify is not found within the cipher", () => {
it("returns null if an identify is not found within the cipher", async () => {
options.cipher.identity = null;
jest.spyOn(autofillService as any, "makeScriptAction");
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
const value = autofillService["generateIdentityFillScript"](
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
@ -3087,432 +3087,389 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
});
it("will not attempt to match custom fields", () => {
const customField = createAutofillFieldMock({ tagName: "span" });
pageDetails.fields.push(customField);
let isRefactorFeatureFlagSet = false;
for (let index = 0; index < 2; index++) {
describe(`when the isRefactorFeatureFlagSet is ${isRefactorFeatureFlagSet}`, () => {
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(isRefactorFeatureFlagSet);
});
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
afterAll(() => {
isRefactorFeatureFlagSet = true;
});
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
it("will not attempt to match custom fields", async () => {
const customField = createAutofillFieldMock({ tagName: "span" });
pageDetails.fields.push(customField);
it("will not attempt to match a field that is of an excluded type", () => {
const excludedField = createAutofillFieldMock({ type: "hidden" });
pageDetails.fields.push(excludedField);
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
excludedField,
AutoFillConstants.ExcludedAutofillTypes,
);
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
it("will not attempt to match a field that is of an excluded type", async () => {
const excludedField = createAutofillFieldMock({ type: "hidden" });
pageDetails.fields.push(excludedField);
it("will not attempt to match a field that is not viewable", () => {
const viewableField = createAutofillFieldMock({ viewable: false });
pageDetails.fields.push(viewableField);
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
excludedField,
AutoFillConstants.ExcludedAutofillTypes,
);
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
it("will not attempt to match a field that is not viewable", async () => {
const viewableField = createAutofillFieldMock({ viewable: false });
pageDetails.fields.push(viewableField);
it("will match a full name field to the vault item identity value", () => {
const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" });
pageDetails.fields = [fullNameField];
options.cipher.identity.firstName = firstName;
options.cipher.identity.middleName = middleName;
options.cipher.identity.lastName = lastName;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
fullNameField.htmlName,
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
`${firstName} ${middleName} ${lastName}`,
fullNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
fullNameField.opid,
`${firstName} ${middleName} ${lastName}`,
]);
});
it("will match a full name field to the vault item identity value", async () => {
const fullNameField = createAutofillFieldMock({
opid: "fullName",
htmlName: "full-name",
});
pageDetails.fields = [fullNameField];
options.cipher.identity.firstName = firstName;
options.cipher.identity.middleName = middleName;
options.cipher.identity.lastName = lastName;
it("will match a full name field to the a vault item that only has a last name", () => {
const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" });
pageDetails.fields = [fullNameField];
options.cipher.identity.firstName = "";
options.cipher.identity.middleName = "";
options.cipher.identity.lastName = lastName;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
`${firstName} ${middleName} ${lastName}`,
fullNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
fullNameField.opid,
`${firstName} ${middleName} ${lastName}`,
]);
});
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
fullNameField.htmlName,
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
lastName,
fullNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]);
});
it("will match a full name field to the a vault item that only has a last name", async () => {
const fullNameField = createAutofillFieldMock({
opid: "fullName",
htmlName: "full-name",
});
pageDetails.fields = [fullNameField];
options.cipher.identity.firstName = "";
options.cipher.identity.middleName = "";
options.cipher.identity.lastName = lastName;
it("will match first name, middle name, and last name fields to the vault item identity value", () => {
const firstNameField = createAutofillFieldMock({
opid: "firstName",
htmlName: "first-name",
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
lastName,
fullNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]);
});
it("will match first name, middle name, and last name fields to the vault item identity value", async () => {
const firstNameField = createAutofillFieldMock({
opid: "firstName",
htmlName: "first-name",
});
const middleNameField = createAutofillFieldMock({
opid: "middleName",
htmlName: "middle-name",
});
const lastNameField = createAutofillFieldMock({
opid: "lastName",
htmlName: "last-name",
});
pageDetails.fields = [firstNameField, middleNameField, lastNameField];
options.cipher.identity.firstName = firstName;
options.cipher.identity.middleName = middleName;
options.cipher.identity.lastName = lastName;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity.firstName,
firstNameField,
filledFields,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity.middleName,
middleNameField,
filledFields,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity.lastName,
lastNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]);
expect(value.script[5]).toStrictEqual([
"fill_by_opid",
middleNameField.opid,
middleName,
]);
expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]);
});
it("will match title and email fields to the vault item identity value", async () => {
const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" });
const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" });
pageDetails.fields = [titleField, emailField];
const title = "Mr.";
const email = "email@example.com";
options.cipher.identity.title = title;
options.cipher.identity.email = email;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity.title,
titleField,
filledFields,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity.email,
emailField,
filledFields,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]);
});
it("will match a full address field to the vault item identity values", async () => {
const fullAddressField = createAutofillFieldMock({
opid: "fullAddress",
htmlName: "address",
});
pageDetails.fields = [fullAddressField];
const address1 = "123 Main St.";
const address2 = "Apt. 1";
const address3 = "P.O. Box 123";
options.cipher.identity.address1 = address1;
options.cipher.identity.address2 = address2;
options.cipher.identity.address3 = address3;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
`${address1}, ${address2}, ${address3}`,
fullAddressField,
filledFields,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
fullAddressField.opid,
`${address1}, ${address2}, ${address3}`,
]);
});
it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", async () => {
const address1Field = createAutofillFieldMock({
opid: "address1",
htmlName: "address-1",
});
const address2Field = createAutofillFieldMock({
opid: "address2",
htmlName: "address-2",
});
const address3Field = createAutofillFieldMock({
opid: "address3",
htmlName: "address-3",
});
const postalCodeField = createAutofillFieldMock({
opid: "postalCode",
htmlName: "postal-code",
});
const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" });
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" });
const usernameField = createAutofillFieldMock({
opid: "username",
htmlName: "username",
});
const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" });
pageDetails.fields = [
address1Field,
address2Field,
address3Field,
postalCodeField,
cityField,
stateField,
countryField,
phoneField,
usernameField,
companyField,
];
const address1 = "123 Main St.";
const address2 = "Apt. 1";
const address3 = "P.O. Box 123";
const postalCode = "12345";
const city = "City";
const state = "TX";
const country = "US";
const phone = "123-456-7890";
const username = "username";
const company = "Company";
options.cipher.identity.address1 = address1;
options.cipher.identity.address2 = address2;
options.cipher.identity.address3 = address3;
options.cipher.identity.postalCode = postalCode;
options.cipher.identity.city = city;
options.cipher.identity.state = state;
options.cipher.identity.country = country;
options.cipher.identity.phone = phone;
options.cipher.identity.username = username;
options.cipher.identity.company = company;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script).toContainEqual(["fill_by_opid", address1Field.opid, address1]);
expect(value.script).toContainEqual(["fill_by_opid", address2Field.opid, address2]);
expect(value.script).toContainEqual(["fill_by_opid", address3Field.opid, address3]);
expect(value.script).toContainEqual(["fill_by_opid", postalCodeField.opid, postalCode]);
expect(value.script).toContainEqual(["fill_by_opid", cityField.opid, city]);
expect(value.script).toContainEqual(["fill_by_opid", stateField.opid, state]);
expect(value.script).toContainEqual(["fill_by_opid", countryField.opid, country]);
expect(value.script).toContainEqual(["fill_by_opid", phoneField.opid, phone]);
expect(value.script).toContainEqual(["fill_by_opid", usernameField.opid, username]);
expect(value.script).toContainEqual(["fill_by_opid", companyField.opid, company]);
});
it("will find the two character IsoState value for an identity cipher that contains the full name of a state", async () => {
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
pageDetails.fields = [stateField];
const state = "California";
options.cipher.identity.state = state;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"CA",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]);
});
it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", async () => {
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
pageDetails.fields = [stateField];
const state = "Ontario";
options.cipher.identity.state = state;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"ON",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]);
});
it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", async () => {
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
pageDetails.fields = [countryField];
const country = "Somalia";
options.cipher.identity.country = country;
const value = await autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"SO",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]);
});
});
const middleNameField = createAutofillFieldMock({
opid: "middleName",
htmlName: "middle-name",
});
const lastNameField = createAutofillFieldMock({ opid: "lastName", htmlName: "last-name" });
pageDetails.fields = [firstNameField, middleNameField, lastNameField];
options.cipher.identity.firstName = firstName;
options.cipher.identity.middleName = middleName;
options.cipher.identity.lastName = lastName;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
firstNameField.htmlName,
IdentityAutoFillConstants.FirstnameFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
middleNameField.htmlName,
IdentityAutoFillConstants.MiddlenameFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
lastNameField.htmlName,
IdentityAutoFillConstants.LastnameFieldNames,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
firstNameField.opid,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
middleNameField.opid,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
lastNameField.opid,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", middleNameField.opid, middleName]);
expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]);
});
it("will match title and email fields to the vault item identity value", () => {
const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" });
const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" });
pageDetails.fields = [titleField, emailField];
const title = "Mr.";
const email = "email@example.com";
options.cipher.identity.title = title;
options.cipher.identity.email = email;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
titleField.htmlName,
IdentityAutoFillConstants.TitleFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
emailField.htmlName,
IdentityAutoFillConstants.EmailFieldNames,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
titleField.opid,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
emailField.opid,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]);
});
it("will match a full address field to the vault item identity values", () => {
const fullAddressField = createAutofillFieldMock({
opid: "fullAddress",
htmlName: "address",
});
pageDetails.fields = [fullAddressField];
const address1 = "123 Main St.";
const address2 = "Apt. 1";
const address3 = "P.O. Box 123";
options.cipher.identity.address1 = address1;
options.cipher.identity.address2 = address2;
options.cipher.identity.address3 = address3;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
fullAddressField.htmlName,
IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
`${address1}, ${address2}, ${address3}`,
fullAddressField,
filledFields,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
fullAddressField.opid,
`${address1}, ${address2}, ${address3}`,
]);
});
it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", () => {
const address1Field = createAutofillFieldMock({ opid: "address1", htmlName: "address-1" });
const address2Field = createAutofillFieldMock({ opid: "address2", htmlName: "address-2" });
const address3Field = createAutofillFieldMock({ opid: "address3", htmlName: "address-3" });
const postalCodeField = createAutofillFieldMock({
opid: "postalCode",
htmlName: "postal-code",
});
const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" });
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" });
const usernameField = createAutofillFieldMock({ opid: "username", htmlName: "username" });
const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" });
pageDetails.fields = [
address1Field,
address2Field,
address3Field,
postalCodeField,
cityField,
stateField,
countryField,
phoneField,
usernameField,
companyField,
];
const address1 = "123 Main St.";
const address2 = "Apt. 1";
const address3 = "P.O. Box 123";
const postalCode = "12345";
const city = "City";
const state = "State";
const country = "Country";
const phone = "123-456-7890";
const username = "username";
const company = "Company";
options.cipher.identity.address1 = address1;
options.cipher.identity.address2 = address2;
options.cipher.identity.address3 = address3;
options.cipher.identity.postalCode = postalCode;
options.cipher.identity.city = city;
options.cipher.identity.state = state;
options.cipher.identity.country = country;
options.cipher.identity.phone = phone;
options.cipher.identity.username = username;
options.cipher.identity.company = company;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
address1Field.htmlName,
IdentityAutoFillConstants.Address1FieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
address2Field.htmlName,
IdentityAutoFillConstants.Address2FieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
address3Field.htmlName,
IdentityAutoFillConstants.Address3FieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
postalCodeField.htmlName,
IdentityAutoFillConstants.PostalCodeFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
cityField.htmlName,
IdentityAutoFillConstants.CityFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
stateField.htmlName,
IdentityAutoFillConstants.StateFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
countryField.htmlName,
IdentityAutoFillConstants.CountryFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
phoneField.htmlName,
IdentityAutoFillConstants.PhoneFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
usernameField.htmlName,
IdentityAutoFillConstants.UserNameFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
companyField.htmlName,
IdentityAutoFillConstants.CompanyFieldNames,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalled();
expect(value.script[2]).toStrictEqual(["fill_by_opid", address1Field.opid, address1]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", address2Field.opid, address2]);
expect(value.script[8]).toStrictEqual(["fill_by_opid", address3Field.opid, address3]);
expect(value.script[11]).toStrictEqual(["fill_by_opid", cityField.opid, city]);
expect(value.script[14]).toStrictEqual(["fill_by_opid", postalCodeField.opid, postalCode]);
expect(value.script[17]).toStrictEqual(["fill_by_opid", companyField.opid, company]);
expect(value.script[20]).toStrictEqual(["fill_by_opid", phoneField.opid, phone]);
expect(value.script[23]).toStrictEqual(["fill_by_opid", usernameField.opid, username]);
expect(value.script[26]).toStrictEqual(["fill_by_opid", stateField.opid, state]);
expect(value.script[29]).toStrictEqual(["fill_by_opid", countryField.opid, country]);
});
it("will find the two character IsoState value for an identity cipher that contains the full name of a state", () => {
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
pageDetails.fields = [stateField];
const state = "California";
options.cipher.identity.state = state;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"CA",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]);
});
it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", () => {
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
pageDetails.fields = [stateField];
const state = "Ontario";
options.cipher.identity.state = state;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"ON",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]);
});
it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", () => {
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
pageDetails.fields = [countryField];
const country = "Somalia";
options.cipher.identity.country = country;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"SO",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]);
});
}
});
});

View File

@ -26,6 +26,7 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { BrowserApi } from "../../platform/browser/browser-api";
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
@ -478,6 +479,12 @@ export default class AutofillService implements AutofillServiceInterface {
return totpCode;
}
/**
* Checks if the cipher requires password reprompt and opens the password reprompt popout if necessary.
*
* @param cipher - The cipher to autofill
* @param tab - The tab to autofill
*/
async isPasswordRepromptRequired(cipher: CipherView, tab: chrome.tabs.Tab): Promise<boolean> {
const userHasMasterPasswordAndKeyHash =
await this.userVerificationService.hasMasterPasswordAndMasterKeyHash();
@ -654,7 +661,7 @@ export default class AutofillService implements AutofillServiceInterface {
fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options);
break;
case CipherType.Identity:
fillScript = this.generateIdentityFillScript(
fillScript = await this.generateIdentityFillScript(
fillScript,
pageDetails,
filledFields,
@ -1243,12 +1250,16 @@ export default class AutofillService implements AutofillServiceInterface {
* @returns {AutofillScript}
* @private
*/
private generateIdentityFillScript(
private async generateIdentityFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: GenerateFillScriptOptions,
): AutofillScript {
): Promise<AutofillScript> {
if (await this.configService.getFeatureFlag(FeatureFlag.GenerateIdentityFillScriptRefactor)) {
return this._generateIdentityFillScript(fillScript, pageDetails, filledFields, options);
}
if (!options.cipher.identity) {
return null;
}
@ -1476,6 +1487,589 @@ export default class AutofillService implements AutofillServiceInterface {
return fillScript;
}
/**
* Generates the autofill script for the specified page details and identity cipher item.
*
* @param fillScript - Object to store autofill script, passed between method references
* @param pageDetails - The details of the page to autofill
* @param filledFields - The fields that have already been filled, passed between method references
* @param options - Contains data used to fill cipher items
*/
private _generateIdentityFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: GenerateFillScriptOptions,
): AutofillScript {
const identity = options.cipher.identity;
if (!identity) {
return null;
}
for (let fieldsIndex = 0; fieldsIndex < pageDetails.fields.length; fieldsIndex++) {
const field = pageDetails.fields[fieldsIndex];
if (this.excludeFieldFromIdentityFill(field)) {
continue;
}
const keywordsList = this.getIdentityAutofillFieldKeywords(field);
const keywordsCombined = keywordsList.join(",");
if (this.shouldMakeIdentityTitleFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.title, field, filledFields);
continue;
}
if (this.shouldMakeIdentityNameFillScript(filledFields, keywordsList)) {
this.makeIdentityNameFillScript(fillScript, filledFields, field, identity);
continue;
}
if (this.shouldMakeIdentityFirstNameFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.firstName, field, filledFields);
continue;
}
if (this.shouldMakeIdentityMiddleNameFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.middleName, field, filledFields);
continue;
}
if (this.shouldMakeIdentityLastNameFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.lastName, field, filledFields);
continue;
}
if (this.shouldMakeIdentityEmailFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.email, field, filledFields);
continue;
}
if (this.shouldMakeIdentityAddressFillScript(filledFields, keywordsList)) {
this.makeIdentityAddressFillScript(fillScript, filledFields, field, identity);
continue;
}
if (this.shouldMakeIdentityAddress1FillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.address1, field, filledFields);
continue;
}
if (this.shouldMakeIdentityAddress2FillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.address2, field, filledFields);
continue;
}
if (this.shouldMakeIdentityAddress3FillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.address3, field, filledFields);
continue;
}
if (this.shouldMakeIdentityPostalCodeFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.postalCode, field, filledFields);
continue;
}
if (this.shouldMakeIdentityCityFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.city, field, filledFields);
continue;
}
if (this.shouldMakeIdentityStateFillScript(filledFields, keywordsCombined)) {
this.makeIdentityStateFillScript(fillScript, filledFields, field, identity);
continue;
}
if (this.shouldMakeIdentityCountryFillScript(filledFields, keywordsCombined)) {
this.makeIdentityCountryFillScript(fillScript, filledFields, field, identity);
continue;
}
if (this.shouldMakeIdentityPhoneFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.phone, field, filledFields);
continue;
}
if (this.shouldMakeIdentityUserNameFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.username, field, filledFields);
continue;
}
if (this.shouldMakeIdentityCompanyFillScript(filledFields, keywordsCombined)) {
this.makeScriptActionWithValue(fillScript, identity.company, field, filledFields);
}
}
return fillScript;
}
/**
* Identifies if the current field should be excluded from triggering autofill of the identity cipher.
*
* @param field - The field to check
*/
private excludeFieldFromIdentityFill(field: AutofillField): boolean {
return (
AutofillService.isExcludedFieldType(field, AutoFillConstants.ExcludedAutofillTypes) ||
AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) ||
!field.viewable
);
}
/**
* Gathers all unique keyword identifiers from a field that can be used to determine what
* identity value should be filled.
*
* @param field - The field to gather keywords from
*/
private getIdentityAutofillFieldKeywords(field: AutofillField): string[] {
const keywords: Set<string> = new Set();
for (let index = 0; index < IdentityAutoFillConstants.IdentityAttributes.length; index++) {
const attribute = IdentityAutoFillConstants.IdentityAttributes[index];
if (field[attribute]) {
keywords.add(
field[attribute]
.trim()
.toLowerCase()
.replace(/[^a-zA-Z0-9]+/g, ""),
);
}
}
return Array.from(keywords);
}
/**
* Identifies if a fill script action for the identity title
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityTitleFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.title &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.TitleFieldNames)
);
}
/**
* Identifies if a fill script action for the identity name
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityNameFillScript(
filledFields: Record<string, AutofillField>,
keywords: string[],
): boolean {
return (
!filledFields.name &&
keywords.some((keyword) =>
AutofillService.isFieldMatch(
keyword,
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues,
),
)
);
}
/**
* Identifies if a fill script action for the identity first name
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityFirstNameFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.firstName &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.FirstnameFieldNames)
);
}
/**
* Identifies if a fill script action for the identity middle name
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityMiddleNameFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.middleName &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.MiddlenameFieldNames)
);
}
/**
* Identifies if a fill script action for the identity last name
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityLastNameFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.lastName &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.LastnameFieldNames)
);
}
/**
* Identifies if a fill script action for the identity email
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityEmailFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.email &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.EmailFieldNames)
);
}
/**
* Identifies if a fill script action for the identity address
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityAddressFillScript(
filledFields: Record<string, AutofillField>,
keywords: string[],
): boolean {
return (
!filledFields.address &&
keywords.some((keyword) =>
AutofillService.isFieldMatch(
keyword,
IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues,
),
)
);
}
/**
* Identifies if a fill script action for the identity address1
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityAddress1FillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.address1 &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address1FieldNames)
);
}
/**
* Identifies if a fill script action for the identity address2
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityAddress2FillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.address2 &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address2FieldNames)
);
}
/**
* Identifies if a fill script action for the identity address3
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityAddress3FillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.address3 &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address3FieldNames)
);
}
/**
* Identifies if a fill script action for the identity postal code
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityPostalCodeFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.postalCode &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PostalCodeFieldNames)
);
}
/**
* Identifies if a fill script action for the identity city
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityCityFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.city &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CityFieldNames)
);
}
/**
* Identifies if a fill script action for the identity state
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityStateFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.state &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.StateFieldNames)
);
}
/**
* Identifies if a fill script action for the identity country
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityCountryFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.country &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CountryFieldNames)
);
}
/**
* Identifies if a fill script action for the identity phone
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityPhoneFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.phone &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PhoneFieldNames)
);
}
/**
* Identifies if a fill script action for the identity username
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityUserNameFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.username &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.UserNameFieldNames)
);
}
/**
* Identifies if a fill script action for the identity company
* field should be created for the provided field.
*
* @param filledFields - The fields that have already been filled
* @param keywords - The keywords from the field
*/
private shouldMakeIdentityCompanyFillScript(
filledFields: Record<string, AutofillField>,
keywords: string,
): boolean {
return (
!filledFields.company &&
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CompanyFieldNames)
);
}
/**
* Creates an identity name fill script action for the provided field. This is used
* when filling a `full name` field, using the first, middle, and last name from the
* identity cipher item.
*
* @param fillScript - The autofill script to add the action to
* @param filledFields - The fields that have already been filled
* @param field - The field to fill
* @param identity - The identity cipher item
*/
private makeIdentityNameFillScript(
fillScript: AutofillScript,
filledFields: Record<string, AutofillField>,
field: AutofillField,
identity: IdentityView,
) {
let name = "";
if (identity.firstName) {
name += identity.firstName;
}
if (identity.middleName) {
name += !name ? identity.middleName : ` ${identity.middleName}`;
}
if (identity.lastName) {
name += !name ? identity.lastName : ` ${identity.lastName}`;
}
this.makeScriptActionWithValue(fillScript, name, field, filledFields);
}
/**
* Creates an identity address fill script action for the provided field. This is used
* when filling a generic `address` field, using the address1, address2, and address3
* from the identity cipher item.
*
* @param fillScript - The autofill script to add the action to
* @param filledFields - The fields that have already been filled
* @param field - The field to fill
* @param identity - The identity cipher item
*/
private makeIdentityAddressFillScript(
fillScript: AutofillScript,
filledFields: Record<string, AutofillField>,
field: AutofillField,
identity: IdentityView,
) {
if (!identity.address1) {
return;
}
let address = identity.address1;
if (identity.address2) {
address += `, ${identity.address2}`;
}
if (identity.address3) {
address += `, ${identity.address3}`;
}
this.makeScriptActionWithValue(fillScript, address, field, filledFields);
}
/**
* Creates an identity state fill script action for the provided field. This is used
* when filling a `state` field, using the state value from the identity cipher item.
* If the state value is a full name, it will be converted to an ISO code.
*
* @param fillScript - The autofill script to add the action to
* @param filledFields - The fields that have already been filled
* @param field - The field to fill
* @param identity - The identity cipher item
*/
private makeIdentityStateFillScript(
fillScript: AutofillScript,
filledFields: Record<string, AutofillField>,
field: AutofillField,
identity: IdentityView,
) {
if (!identity.state) {
return;
}
if (identity.state.length <= 2) {
this.makeScriptActionWithValue(fillScript, identity.state, field, filledFields);
return;
}
const stateLower = identity.state.toLowerCase();
const isoState =
IdentityAutoFillConstants.IsoStates[stateLower] ||
IdentityAutoFillConstants.IsoProvinces[stateLower];
if (isoState) {
this.makeScriptActionWithValue(fillScript, isoState, field, filledFields);
}
}
/**
* Creates an identity country fill script action for the provided field. This is used
* when filling a `country` field, using the country value from the identity cipher item.
* If the country value is a full name, it will be converted to an ISO code.
*
* @param fillScript - The autofill script to add the action to
* @param filledFields - The fields that have already been filled
* @param field - The field to fill
* @param identity - The identity cipher item
*/
private makeIdentityCountryFillScript(
fillScript: AutofillScript,
filledFields: Record<string, AutofillField>,
field: AutofillField,
identity: IdentityView,
) {
if (!identity.country) {
return;
}
if (identity.country.length <= 2) {
this.makeScriptActionWithValue(fillScript, identity.country, field, filledFields);
return;
}
const countryLower = identity.country.toLowerCase();
const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower];
if (isoCountry) {
this.makeScriptActionWithValue(fillScript, isoCountry, field, filledFields);
}
}
/**
* Accepts an HTMLInputElement type value and a list of
* excluded types and returns true if the type is excluded.

View File

@ -1057,7 +1057,7 @@ export class InlineMenuFieldQualificationService
returnStringValue: boolean,
) {
if (!this.autofillFieldKeywordsMap.has(autofillFieldData)) {
const keywords = [
const keywordsSet = new Set<string>([
autofillFieldData.htmlID,
autofillFieldData.htmlName,
autofillFieldData.htmlClass,
@ -1071,9 +1071,8 @@ export class InlineMenuFieldQualificationService
autofillFieldData["label-right"],
autofillFieldData["label-tag"],
autofillFieldData["label-top"],
];
const keywordsSet = new Set<string>(keywords);
const stringValue = keywords.join(",").toLowerCase();
]);
const stringValue = Array.from(keywordsSet).join(",").toLowerCase();
this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue });
}

View File

@ -29,6 +29,7 @@ export enum FeatureFlag {
AuthenticatorTwoFactorToken = "authenticator-2fa-token",
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@ -68,6 +69,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.AuthenticatorTwoFactorToken]: FALSE,
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;