Skip to content

Commit e23b2d0

Browse files
Autofill/pm 25597 plex password generation (#16997)
* Correctly fill generated passwords and current password on plex.tv * Correctly fill generated passwords and current password on plex.tv * Leave existing forEach * Add tests for changes
1 parent a5caa19 commit e23b2d0

File tree

6 files changed

+259
-18
lines changed

6 files changed

+259
-18
lines changed

apps/browser/src/autofill/background/overlay.background.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3286,6 +3286,9 @@ describe("OverlayBackground", () => {
32863286
pageDetails: [pageDetailsForTab],
32873287
fillNewPassword: true,
32883288
allowTotpAutofill: true,
3289+
focusedFieldForm: undefined,
3290+
focusedFieldOpid: undefined,
3291+
inlineMenuFillType: undefined,
32893292
});
32903293
expect(overlayBackground["inlineMenuCiphers"].entries()).toStrictEqual(
32913294
new Map([
@@ -3680,6 +3683,9 @@ describe("OverlayBackground", () => {
36803683
pageDetails: [overlayBackground["pageDetailsForTab"][sender.tab.id].get(sender.frameId)],
36813684
fillNewPassword: true,
36823685
allowTotpAutofill: false,
3686+
focusedFieldForm: undefined,
3687+
focusedFieldOpid: undefined,
3688+
inlineMenuFillType: InlineMenuFillTypes.PasswordGeneration,
36833689
});
36843690
});
36853691
});

apps/browser/src/autofill/background/overlay.background.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
11771177
allowTotpAutofill: true,
11781178
focusedFieldForm: this.focusedFieldData?.focusedFieldForm,
11791179
focusedFieldOpid: this.focusedFieldData?.focusedFieldOpid,
1180+
inlineMenuFillType: this.focusedFieldData?.inlineMenuFillType,
11801181
});
11811182

11821183
if (totpCode) {
@@ -1863,6 +1864,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
18631864
allowTotpAutofill: false,
18641865
focusedFieldForm: this.focusedFieldData?.focusedFieldForm,
18651866
focusedFieldOpid: this.focusedFieldData?.focusedFieldOpid,
1867+
inlineMenuFillType: InlineMenuFillTypes.PasswordGeneration,
18661868
});
18671869

18681870
globalThis.setTimeout(async () => {

apps/browser/src/autofill/services/abstractions/autofill.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
66
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
77

88
import { AutofillMessageCommand } from "../../enums/autofill-message.enums";
9+
import { InlineMenuFillType } from "../../enums/autofill-overlay.enum";
910
import AutofillField from "../../models/autofill-field";
1011
import AutofillForm from "../../models/autofill-form";
1112
import AutofillPageDetails from "../../models/autofill-page-details";
@@ -30,6 +31,7 @@ export interface AutoFillOptions {
3031
autoSubmitLogin?: boolean;
3132
focusedFieldForm?: string;
3233
focusedFieldOpid?: string;
34+
inlineMenuFillType?: InlineMenuFillType;
3335
}
3436

3537
export interface FormData {
@@ -49,6 +51,7 @@ export interface GenerateFillScriptOptions {
4951
tabUrl: string;
5052
defaultUriMatch: UriMatchStrategySetting;
5153
focusedFieldOpid?: string;
54+
inlineMenuFillType?: InlineMenuFillType;
5255
}
5356

5457
export type CollectPageDetailsResponseMessage = {

apps/browser/src/autofill/services/autofill-overlay-content.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
11181118
* @param autofillFieldData - Autofill field data captured from the form field element.
11191119
*/
11201120
private async setQualifiedLoginFillType(autofillFieldData: AutofillField) {
1121+
// Check if this is a current password field in a password change form
1122+
if (this.inlineMenuFieldQualificationService.isUpdateCurrentPasswordField(autofillFieldData)) {
1123+
autofillFieldData.inlineMenuFillType = InlineMenuFillTypes.CurrentPasswordUpdate;
1124+
return;
1125+
}
1126+
11211127
autofillFieldData.inlineMenuFillType = CipherType.Login;
11221128
autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn");
11231129

apps/browser/src/autofill/services/autofill.service.spec.ts

Lines changed: 202 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
4444
import { BrowserApi } from "../../platform/browser/browser-api";
4545
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
4646
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
47+
import { InlineMenuFillTypes } from "../enums/autofill-overlay.enum";
4748
import { AutofillPort } from "../enums/autofill-port.enum";
4849
import AutofillField from "../models/autofill-field";
4950
import AutofillPageDetails from "../models/autofill-page-details";
@@ -103,6 +104,15 @@ describe("AutofillService", () => {
103104
beforeEach(() => {
104105
configService = mock<ConfigService>();
105106
configService.getFeatureFlag$.mockImplementation(() => of(false));
107+
108+
// Initialize domainSettingsService BEFORE it's used
109+
domainSettingsService = new DefaultDomainSettingsService(
110+
fakeStateProvider,
111+
policyService,
112+
accountService,
113+
);
114+
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
115+
106116
scriptInjectorService = new BrowserScriptInjectorService(
107117
domainSettingsService,
108118
platformUtilsService,
@@ -141,12 +151,6 @@ describe("AutofillService", () => {
141151
userNotificationsSettings,
142152
messageListener,
143153
);
144-
domainSettingsService = new DefaultDomainSettingsService(
145-
fakeStateProvider,
146-
policyService,
147-
accountService,
148-
);
149-
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
150154
jest.spyOn(BrowserApi, "tabSendMessage");
151155
});
152156

@@ -2077,6 +2081,193 @@ describe("AutofillService", () => {
20772081
});
20782082
});
20792083

2084+
describe("given password generation with inlineMenuFillType", () => {
2085+
beforeEach(() => {
2086+
pageDetails.forms = undefined;
2087+
pageDetails.fields = []; // Clear fields to start fresh
2088+
options.inlineMenuFillType = InlineMenuFillTypes.PasswordGeneration;
2089+
options.cipher.login.totp = null; // Disable TOTP for these tests
2090+
});
2091+
2092+
it("includes all password fields from the same form when filling with password generation", async () => {
2093+
const newPasswordField = createAutofillFieldMock({
2094+
opid: "new-password",
2095+
type: "password",
2096+
form: "validFormId",
2097+
elementNumber: 2,
2098+
});
2099+
const confirmPasswordField = createAutofillFieldMock({
2100+
opid: "confirm-password",
2101+
type: "password",
2102+
form: "validFormId",
2103+
elementNumber: 3,
2104+
});
2105+
pageDetails.fields.push(newPasswordField, confirmPasswordField);
2106+
options.focusedFieldOpid = newPasswordField.opid;
2107+
2108+
await autofillService["generateLoginFillScript"](
2109+
fillScript,
2110+
pageDetails,
2111+
filledFields,
2112+
options,
2113+
);
2114+
2115+
expect(filledFields[newPasswordField.opid]).toBeDefined();
2116+
expect(filledFields[confirmPasswordField.opid]).toBeDefined();
2117+
});
2118+
2119+
it("finds username field for the first password field when generating passwords", async () => {
2120+
const newPasswordField = createAutofillFieldMock({
2121+
opid: "new-password",
2122+
type: "password",
2123+
form: "validFormId",
2124+
elementNumber: 2,
2125+
});
2126+
pageDetails.fields.push(newPasswordField);
2127+
options.focusedFieldOpid = newPasswordField.opid;
2128+
jest.spyOn(autofillService as any, "findUsernameField");
2129+
2130+
await autofillService["generateLoginFillScript"](
2131+
fillScript,
2132+
pageDetails,
2133+
filledFields,
2134+
options,
2135+
);
2136+
2137+
expect(autofillService["findUsernameField"]).toHaveBeenCalledWith(
2138+
pageDetails,
2139+
expect.objectContaining({ opid: newPasswordField.opid }),
2140+
false,
2141+
false,
2142+
true,
2143+
);
2144+
});
2145+
2146+
it("does not include password fields from different forms", async () => {
2147+
const formAPasswordField = createAutofillFieldMock({
2148+
opid: "form-a-password",
2149+
type: "password",
2150+
form: "formA",
2151+
elementNumber: 1,
2152+
});
2153+
const formBPasswordField = createAutofillFieldMock({
2154+
opid: "form-b-password",
2155+
type: "password",
2156+
form: "formB",
2157+
elementNumber: 2,
2158+
});
2159+
pageDetails.fields = [formAPasswordField, formBPasswordField];
2160+
options.focusedFieldOpid = formAPasswordField.opid;
2161+
2162+
await autofillService["generateLoginFillScript"](
2163+
fillScript,
2164+
pageDetails,
2165+
filledFields,
2166+
options,
2167+
);
2168+
2169+
expect(filledFields[formAPasswordField.opid]).toBeDefined();
2170+
expect(filledFields[formBPasswordField.opid]).toBeUndefined();
2171+
});
2172+
});
2173+
2174+
describe("given current password update with inlineMenuFillType", () => {
2175+
beforeEach(() => {
2176+
pageDetails.forms = undefined;
2177+
pageDetails.fields = []; // Clear fields to start fresh
2178+
options.inlineMenuFillType = InlineMenuFillTypes.CurrentPasswordUpdate;
2179+
options.cipher.login.totp = null; // Disable TOTP for these tests
2180+
});
2181+
2182+
it("includes all password fields from the same form when updating current password", async () => {
2183+
const currentPasswordField = createAutofillFieldMock({
2184+
opid: "current-password",
2185+
type: "password",
2186+
form: "validFormId",
2187+
elementNumber: 1,
2188+
});
2189+
const newPasswordField = createAutofillFieldMock({
2190+
opid: "new-password",
2191+
type: "password",
2192+
form: "validFormId",
2193+
elementNumber: 2,
2194+
});
2195+
const confirmPasswordField = createAutofillFieldMock({
2196+
opid: "confirm-password",
2197+
type: "password",
2198+
form: "validFormId",
2199+
elementNumber: 3,
2200+
});
2201+
pageDetails.fields.push(currentPasswordField, newPasswordField, confirmPasswordField);
2202+
options.focusedFieldOpid = currentPasswordField.opid;
2203+
2204+
await autofillService["generateLoginFillScript"](
2205+
fillScript,
2206+
pageDetails,
2207+
filledFields,
2208+
options,
2209+
);
2210+
2211+
expect(filledFields[currentPasswordField.opid]).toBeDefined();
2212+
expect(filledFields[newPasswordField.opid]).toBeDefined();
2213+
expect(filledFields[confirmPasswordField.opid]).toBeDefined();
2214+
});
2215+
2216+
it("includes all password fields from the same form without TOTP", async () => {
2217+
const currentPasswordField = createAutofillFieldMock({
2218+
opid: "current-password",
2219+
type: "password",
2220+
form: "validFormId",
2221+
elementNumber: 1,
2222+
});
2223+
const newPasswordField = createAutofillFieldMock({
2224+
opid: "new-password",
2225+
type: "password",
2226+
form: "validFormId",
2227+
elementNumber: 2,
2228+
});
2229+
pageDetails.fields.push(currentPasswordField, newPasswordField);
2230+
options.focusedFieldOpid = currentPasswordField.opid;
2231+
2232+
await autofillService["generateLoginFillScript"](
2233+
fillScript,
2234+
pageDetails,
2235+
filledFields,
2236+
options,
2237+
);
2238+
2239+
expect(filledFields[currentPasswordField.opid]).toBeDefined();
2240+
expect(filledFields[newPasswordField.opid]).toBeDefined();
2241+
});
2242+
2243+
it("does not include password fields from different forms during password update", async () => {
2244+
const formAPasswordField = createAutofillFieldMock({
2245+
opid: "form-a-password",
2246+
type: "password",
2247+
form: "formA",
2248+
elementNumber: 1,
2249+
});
2250+
const formBPasswordField = createAutofillFieldMock({
2251+
opid: "form-b-password",
2252+
type: "password",
2253+
form: "formB",
2254+
elementNumber: 2,
2255+
});
2256+
pageDetails.fields = [formAPasswordField, formBPasswordField];
2257+
options.focusedFieldOpid = formAPasswordField.opid;
2258+
2259+
await autofillService["generateLoginFillScript"](
2260+
fillScript,
2261+
pageDetails,
2262+
filledFields,
2263+
options,
2264+
);
2265+
2266+
expect(filledFields[formAPasswordField.opid]).toBeDefined();
2267+
expect(filledFields[formBPasswordField.opid]).toBeUndefined();
2268+
});
2269+
});
2270+
20802271
describe("given a set of page details that does not contain a password field", () => {
20812272
let emailField: AutofillField;
20822273
let emailFieldView: FieldView;
@@ -3140,12 +3331,16 @@ describe("AutofillService", () => {
31403331
"example.com",
31413332
"exampleapp.com",
31423333
]);
3143-
domainSettingsService.equivalentDomains$ = of([["not-example.com"]]);
31443334
const pageUrl = "https://subdomain.example.com";
31453335
const tabUrl = "https://www.not-example.com";
31463336
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
31473337
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
31483338

3339+
// Mock getUrlEquivalentDomains to return the expected domains
3340+
jest
3341+
.spyOn(domainSettingsService, "getUrlEquivalentDomains")
3342+
.mockReturnValue(of(equivalentDomains));
3343+
31493344
const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
31503345

31513346
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(

0 commit comments

Comments
 (0)