diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index 2942ba545ea7..0f95cd527eee 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -34,10 +34,23 @@ describe("InlineMenuFieldQualificationService", () => { ); }); + it("has a keyword value that indicates the field is for a create account form", () => { + const field = mock({ + type: "password", + placeholder: "create account password", + autoCompleteType: "", + }); + + expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( + false, + ); + }); + it("has a type that is an excluded type", () => { AutoFillConstants.ExcludedAutofillLoginTypes.forEach((excludedType) => { const field = mock({ type: excludedType, + autoCompleteType: "", }); expect( @@ -53,6 +66,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: index === 0 ? attribute : "", htmlName: index === 1 ? attribute : "", placeholder: index > 1 ? attribute : "", + autoCompleteType: "", }); expect( @@ -67,6 +81,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "not-password", htmlName: "not-password", placeholder: "not-password", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -80,6 +95,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "something-else", htmlName: "something-else", placeholder: "something-else", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -93,6 +109,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "search", htmlName: "something-else", placeholder: "something-else", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -112,12 +129,14 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "", + autoCompleteType: "", }); const secondField = mock({ type: "password", htmlID: "some-other-password", htmlName: "some-other-password", placeholder: "some-other-password", + autoCompleteType: "", }); pageDetails.fields = [field, secondField]; @@ -133,18 +152,21 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "", + autoCompleteType: "", }); const usernameField = mock({ type: "text", htmlID: "user-username", htmlName: "user-username", placeholder: "user-username", + autoCompleteType: "", }); const secondUsernameField = mock({ type: "text", htmlID: "some-other-user-username", htmlName: "some-other-user-username", placeholder: "some-other-user-username", + autoCompleteType: "", }); pageDetails.fields = [field, usernameField, secondUsernameField]; @@ -186,6 +208,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "validFormId", + autoCompleteType: "", }); const secondField = mock({ type: "password", @@ -193,6 +216,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "some-other-password", placeholder: "some-other-password", form: "validFormId", + autoCompleteType: "", }); pageDetails.fields = [field, secondField]; @@ -218,12 +242,35 @@ describe("InlineMenuFieldQualificationService", () => { ); }); + it("is structured on a page with a single set of username and password fields", () => { + const field = mock({ + type: "password", + htmlID: "user-password", + htmlName: "user-password", + placeholder: "user-password", + autoCompleteType: "", + }); + const usernameField = mock({ + type: "text", + htmlID: "user-username", + htmlName: "user-username", + placeholder: "user-username", + autoCompleteType: "", + }); + pageDetails.fields = [field, usernameField]; + + expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( + true, + ); + }); + it("has a type of `text` with an attribute that indicates the field is a password field", () => { const field = mock({ type: "text", htmlID: null, htmlName: "user-password", placeholder: "user-password", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -247,6 +294,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "user-username", htmlName: "user-username", placeholder: "user-username", + autoCompleteType: "", }); pageDetails.fields = [field, usernameField]; @@ -273,6 +321,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "validFormId", + autoCompleteType: "", }); const secondPasswordField = mock({ type: "password", @@ -280,6 +329,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "some-other-password", placeholder: "some-other-password", form: "anotherFormId", + autoCompleteType: "", }); const usernameField = mock({ type: "text", @@ -287,6 +337,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-username", placeholder: "user-username", form: "validFormId", + autoCompleteType: "", }); pageDetails.fields = [field, secondPasswordField, usernameField]; @@ -310,6 +361,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "some-other-password", placeholder: "some-other-password", form: "anotherFormId", + autoCompleteType: "", }); pageDetails.fields = [field, secondPasswordField]; @@ -347,21 +399,23 @@ describe("InlineMenuFieldQualificationService", () => { }); }); - ["new", "change", "neue", "ändern"].forEach((keyword) => { - it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => { - const field = mock({ - type: "text", - autoCompleteType: "", - htmlID: "user-username", - htmlName: "user-username", - placeholder: `${keyword} username`, - }); - - expect( - inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), - ).toBe(false); - }); - }); + ["new", "change", "neue", "ändern", "register", "create", "registration"].forEach( + (keyword) => { + it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => { + const field = mock({ + type: "text", + autoCompleteType: "", + htmlID: "user-username", + htmlName: "user-username", + placeholder: `${keyword} username`, + }); + + expect( + inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), + ).toBe(false); + }); + }, + ); describe("does not have a parent form element", () => { beforeEach(() => { diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 582f8889daad..7bc027b392cf 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -14,9 +14,18 @@ export class InlineMenuFieldQualificationService private usernameAutocompleteValues = new Set(["username", "email"]); private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(","); private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(","); + private currentPasswordAutocompleteValues = new Set(["current-password"]); + private newPasswordAutoCompleteValues = new Set(["new-password"]); private autofillFieldKeywordsMap: WeakMap = new WeakMap(); private autocompleteDisabledValues = new Set(["off", "false"]); private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]); + private accountCreationFieldKeywords = new Set([ + "register", + "registration", + "create", + "confirm", + ...this.newFieldKeywords, + ]); private inlineMenuFieldQualificationFlagSet = false; constructor() { @@ -62,7 +71,12 @@ export class InlineMenuFieldQualificationService ): boolean { // If the provided field is set with an autocomplete value of "current-password", we should assume that // the page developer intends for this field to be interpreted as a password field for a login form. - if (field.autoCompleteType === "current-password") { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.currentPasswordAutocompleteValues, + ) + ) { return true; } @@ -95,7 +109,11 @@ export class InlineMenuFieldQualificationService // If a single username field or less is present on the page, then we can assume that the // provided field is for a login form. This will only be the case if the field does not // explicitly have its autocomplete attribute set to "off" or "false". - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } // If the field has a form parent and there are multiple visible password fields @@ -117,7 +135,10 @@ export class InlineMenuFieldQualificationService // If the field has a form parent and no username field exists and the field has an // autocomplete attribute set to "off" or "false", this is not a password field - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } /** @@ -132,7 +153,9 @@ export class InlineMenuFieldQualificationService ): boolean { // If the provided field is set with an autocomplete of "username", we should assume that // the page developer intends for this field to be interpreted as a username field. - if (this.usernameAutocompleteValues.has(field.autoCompleteType)) { + if ( + this.fieldContainsAutocompleteValues(field.autoCompleteType, this.usernameAutocompleteValues) + ) { const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField); return newPasswordFieldsInPageDetails.length === 0; } @@ -175,7 +198,10 @@ export class InlineMenuFieldQualificationService // If the page does not contain any password fields, it might be part of a multistep login form. // That will only be the case if the field does not explicitly have its autocomplete attribute // set to "off" or "false". - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } // If the field is structured within a form, but no password fields are present in the form, @@ -183,7 +209,12 @@ export class InlineMenuFieldQualificationService if (passwordFieldsInPageDetails.length === 0) { // If the field's autocomplete is set to a disabled value, we should assume that the field is // not part of a login form. - if (this.autocompleteDisabledValues.has(field.autoCompleteType)) { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ) + ) { return false; } @@ -212,7 +243,10 @@ export class InlineMenuFieldQualificationService // If no visible password fields are found, this field might be part of a multipart form. // Check for an invalid autocompleteType to determine if the field is part of a login form. - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } /** @@ -237,7 +271,13 @@ export class InlineMenuFieldQualificationService * @param field - The field to validate */ private isCurrentPasswordField = (field: AutofillField): boolean => { - if (field.autoCompleteType === "new-password") { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.newPasswordAutoCompleteValues, + ) || + this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords]) + ) { return false; } @@ -250,11 +290,19 @@ export class InlineMenuFieldQualificationService * @param field - The field to validate */ private isNewPasswordField = (field: AutofillField): boolean => { - if (field.autoCompleteType === "current-password") { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.currentPasswordAutocompleteValues, + ) + ) { return false; } - return this.isPasswordField(field); + return ( + this.isPasswordField(field) && + this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords]) + ); }; /** @@ -422,6 +470,31 @@ export class InlineMenuFieldQualificationService return keywordValues; } + /** + * Separates the provided field data into space-separated values and checks if any + * of the values are present in the provided set of autocomplete values. + * + * @param fieldAutocompleteValue - The field autocomplete value to validate + * @param compareValues - The set of autocomplete values to check against + */ + private fieldContainsAutocompleteValues( + fieldAutocompleteValue: string, + compareValues: Set, + ) { + if (!fieldAutocompleteValue) { + return false; + } + + const autocompleteValueParts = fieldAutocompleteValue.split(" "); + for (let index = 0; index < autocompleteValueParts.length; index++) { + if (compareValues.has(autocompleteValueParts[index])) { + return true; + } + } + + return false; + } + /** * This method represents the previous rudimentary approach to qualifying fields for login forms. *