1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00:00

[PM-10552] Values input between separate iframes are not populated when adding a cipher from the inline menu (#10401)

* [PM-10554] Inline menu glitches on single form fields that are wrapped within inline menu

* [PM-10554] Adding jest tests to validate expected behavior

* [PM-10554] Adding jest tests to validate expected behavior

* [PM-10552] Vaules input between separate iframes are not populated in add-edit cipher popout

* [PM-10552] Working through issues found when attempting to add ciphers within iframes that trigger a blur event

* [PM-10552] Working through issues found when attempting to add ciphers within iframes that trigger a blur event

* [PM-10552] Fixing broken jest tests due to implementation changes

* [PM-10552] Implementing jest tests to validate behavior within OverlayBackground
This commit is contained in:
Cesar Gonzalez
2024-08-05 12:03:17 -05:00
committed by GitHub
parent fdcf1c7ea2
commit cecfbaeaad
5 changed files with 482 additions and 65 deletions

View File

@@ -95,6 +95,10 @@ export type OverlayAddNewItemMessage = {
identity?: NewIdentityCipherData; identity?: NewIdentityCipherData;
}; };
export type CurrentAddNewItemData = OverlayAddNewItemMessage & {
sender: chrome.runtime.MessageSender;
};
export type CloseInlineMenuMessage = { export type CloseInlineMenuMessage = {
forceCloseInlineMenu?: boolean; forceCloseInlineMenu?: boolean;
overlayElement?: string; overlayElement?: string;

View File

@@ -176,8 +176,12 @@ describe("OverlayBackground", () => {
parentFrameId: getFrameCounter, parentFrameId: getFrameCounter,
}); });
}); });
tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage"); tabsSendMessageSpy = jest
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData"); .spyOn(BrowserApi, "tabSendMessage")
.mockImplementation(() => Promise.resolve());
tabSendMessageDataSpy = jest
.spyOn(BrowserApi, "tabSendMessageData")
.mockImplementation(() => Promise.resolve());
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
getTabSpy = jest.spyOn(BrowserApi, "getTab"); getTabSpy = jest.spyOn(BrowserApi, "getTab");
@@ -838,7 +842,7 @@ describe("OverlayBackground", () => {
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id }); overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@@ -857,7 +861,7 @@ describe("OverlayBackground", () => {
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true, imageEnabled: true,
}, },
id: "inline-menu-cipher-1", id: "inline-menu-cipher-0",
login: { login: {
username: "username-1", username: "username-1",
}, },
@@ -1119,10 +1123,12 @@ describe("OverlayBackground", () => {
let openAddEditVaultItemPopoutSpy: jest.SpyInstance; let openAddEditVaultItemPopoutSpy: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } }); sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
openAddEditVaultItemPopoutSpy = jest openAddEditVaultItemPopoutSpy = jest
.spyOn(overlayBackground as any, "openAddEditVaultItemPopout") .spyOn(overlayBackground as any, "openAddEditVaultItemPopout")
.mockImplementation(); .mockImplementation();
overlayBackground["currentAddNewItemData"] = { sender, addNewCipherType: CipherType.Login };
}); });
it("will not open the add edit popout window if the message does not have a login cipher provided", () => { it("will not open the add edit popout window if the message does not have a login cipher provided", () => {
@@ -1132,6 +1138,28 @@ describe("OverlayBackground", () => {
expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled(); expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled();
}); });
it("resets the currentAddNewItemData to null when a cipher view is not successfully created", async () => {
jest.spyOn(overlayBackground as any, "buildLoginCipherView").mockReturnValue(null);
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
addNewCipherType: CipherType.Login,
login: {
uri: "https://tacos.com",
hostname: "",
username: "username",
password: "password",
},
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(overlayBackground["currentAddNewItemData"]).toBeNull();
});
it("will open the add edit popout window after creating a new cipher", async () => { it("will open the add edit popout window after creating a new cipher", async () => {
sendMockExtensionMessage( sendMockExtensionMessage(
{ {
@@ -1146,6 +1174,7 @@ describe("OverlayBackground", () => {
}, },
sender, sender,
); );
jest.advanceTimersByTime(100);
await flushPromises(); await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@@ -1154,6 +1183,8 @@ describe("OverlayBackground", () => {
}); });
it("creates a new card cipher", async () => { it("creates a new card cipher", async () => {
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card;
sendMockExtensionMessage( sendMockExtensionMessage(
{ {
command: "autofillOverlayAddNewVaultItem", command: "autofillOverlayAddNewVaultItem",
@@ -1169,6 +1200,7 @@ describe("OverlayBackground", () => {
}, },
sender, sender,
); );
jest.advanceTimersByTime(100);
await flushPromises(); await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@@ -1177,6 +1209,10 @@ describe("OverlayBackground", () => {
}); });
describe("creating a new identity cipher", () => { describe("creating a new identity cipher", () => {
beforeEach(() => {
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity;
});
it("populates an identity cipher view and creates it", async () => { it("populates an identity cipher view and creates it", async () => {
sendMockExtensionMessage( sendMockExtensionMessage(
{ {
@@ -1203,6 +1239,7 @@ describe("OverlayBackground", () => {
}, },
sender, sender,
); );
jest.advanceTimersByTime(100);
await flushPromises(); await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@@ -1223,6 +1260,7 @@ describe("OverlayBackground", () => {
}, },
sender, sender,
); );
jest.advanceTimersByTime(100);
await flushPromises(); await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@@ -1241,6 +1279,7 @@ describe("OverlayBackground", () => {
}, },
sender, sender,
); );
jest.advanceTimersByTime(100);
await flushPromises(); await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@@ -1259,11 +1298,173 @@ describe("OverlayBackground", () => {
}, },
sender, sender,
); );
jest.advanceTimersByTime(100);
await flushPromises(); await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
}); });
}); });
describe("pulling cipher data from multiple frames of a tab", () => {
let subFrameSender: MockProxy<chrome.runtime.MessageSender>;
const command = "autofillOverlayAddNewVaultItem";
beforeEach(() => {
subFrameSender = mock<chrome.runtime.MessageSender>({ tab: sender.tab, frameId: 2 });
});
it("combines the login cipher data from all frames", async () => {
const buildLoginCipherViewSpy = jest.spyOn(
overlayBackground as any,
"buildLoginCipherView",
);
const addNewCipherType = CipherType.Login;
const loginCipherData = {
uri: "https://tacos.com",
hostname: "",
username: "username",
password: "",
};
const subFrameLoginCipherData = {
uri: "https://tacos.com",
hostname: "tacos.com",
username: "",
password: "password",
};
sendMockExtensionMessage({ command, addNewCipherType, login: loginCipherData }, sender);
sendMockExtensionMessage(
{ command, addNewCipherType, login: subFrameLoginCipherData },
subFrameSender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(buildLoginCipherViewSpy).toHaveBeenCalledWith({
uri: "https://tacos.com",
hostname: "tacos.com",
username: "username",
password: "password",
});
});
it("combines the card cipher data from all frames", async () => {
const buildCardCipherViewSpy = jest.spyOn(
overlayBackground as any,
"buildCardCipherView",
);
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card;
const addNewCipherType = CipherType.Card;
const cardCipherData = {
cardholderName: "cardholderName",
number: "",
expirationMonth: "",
expirationYear: "",
expirationDate: "12/25",
cvv: "123",
};
const subFrameCardCipherData = {
cardholderName: "",
number: "4242424242424242",
expirationMonth: "12",
expirationYear: "2025",
expirationDate: "",
cvv: "",
};
sendMockExtensionMessage({ command, addNewCipherType, card: cardCipherData }, sender);
sendMockExtensionMessage(
{ command, addNewCipherType, card: subFrameCardCipherData },
subFrameSender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(buildCardCipherViewSpy).toHaveBeenCalledWith({
cardholderName: "cardholderName",
number: "4242424242424242",
expirationMonth: "12",
expirationYear: "2025",
expirationDate: "12/25",
cvv: "123",
});
});
it("combines the identity cipher data from all frames", async () => {
const buildIdentityCipherViewSpy = jest.spyOn(
overlayBackground as any,
"buildIdentityCipherView",
);
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity;
const addNewCipherType = CipherType.Identity;
const identityCipherData = {
title: "title",
firstName: "firstName",
middleName: "middleName",
lastName: "",
fullName: "",
address1: "address1",
address2: "address2",
address3: "address3",
city: "city",
state: "state",
postalCode: "postalCode",
country: "country",
company: "company",
phone: "phone",
email: "email",
username: "username",
};
const subFrameIdentityCipherData = {
title: "",
firstName: "",
middleName: "",
lastName: "lastName",
fullName: "fullName",
address1: "",
address2: "",
address3: "",
city: "",
state: "",
postalCode: "",
country: "",
company: "",
phone: "",
email: "",
username: "",
};
sendMockExtensionMessage(
{ command, addNewCipherType, identity: identityCipherData },
sender,
);
sendMockExtensionMessage(
{ command, addNewCipherType, identity: subFrameIdentityCipherData },
subFrameSender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(buildIdentityCipherViewSpy).toHaveBeenCalledWith({
title: "title",
firstName: "firstName",
middleName: "middleName",
lastName: "lastName",
fullName: "fullName",
address1: "address1",
address2: "address2",
address3: "address3",
city: "city",
state: "state",
postalCode: "postalCode",
country: "country",
company: "company",
phone: "phone",
email: "email",
username: "username",
});
});
});
}); });
describe("checkIsInlineMenuCiphersPopulated message handler", () => { describe("checkIsInlineMenuCiphersPopulated message handler", () => {
@@ -1363,6 +1564,51 @@ describe("OverlayBackground", () => {
showInlineMenuAccountCreation: true, showInlineMenuAccountCreation: true,
}); });
}); });
it("triggers an update of the inline menu ciphers when the new focused field's cipher type does not equal the previous focused field's cipher type", async () => {
const updateOverlayCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers");
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
const focusedFieldData = createFocusedFieldDataMock({
tabId: tab.id,
frameId: sender.frameId,
filledByCipherType: CipherType.Login,
});
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
await flushPromises();
const newFocusedFieldData = createFocusedFieldDataMock({
tabId: tab.id,
frameId: sender.frameId,
filledByCipherType: CipherType.Card,
});
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData: newFocusedFieldData },
sender,
);
await flushPromises();
expect(updateOverlayCiphersSpy).toHaveBeenCalled();
});
});
describe("updateIsFieldCurrentlyFocused message handler", () => {
it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => {
const focusedFieldData = createFocusedFieldDataMock();
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 10 }),
);
overlayBackground["isFieldCurrentlyFocused"] = true;
sendMockExtensionMessage(
{ command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false },
mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 20 }),
);
await flushPromises();
expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true);
});
}); });
describe("updateIsFieldCurrentlyFocused message handler", () => { describe("updateIsFieldCurrentlyFocused message handler", () => {
@@ -1841,7 +2087,6 @@ describe("OverlayBackground", () => {
overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([ overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([
[focusedFieldData.frameId, null], [focusedFieldData.frameId, null],
]); ]);
tabsSendMessageSpy.mockImplementation();
jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent"); jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent");
sendMockExtensionMessage( sendMockExtensionMessage(
@@ -2090,7 +2335,6 @@ describe("OverlayBackground", () => {
describe("autofillInlineMenuButtonClicked message handler", () => { describe("autofillInlineMenuButtonClicked message handler", () => {
it("opens the unlock vault popout if the user auth status is not unlocked", async () => { it("opens the unlock vault popout if the user auth status is not unlocked", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked); activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsSendMessageSpy.mockImplementation();
sendPortMessage(buttonMessageConnectorSpy, { sendPortMessage(buttonMessageConnectorSpy, {
command: "autofillInlineMenuButtonClicked", command: "autofillInlineMenuButtonClicked",
@@ -2291,7 +2535,6 @@ describe("OverlayBackground", () => {
describe("unlockVault message handler", () => { describe("unlockVault message handler", () => {
it("opens the unlock vault popout", async () => { it("opens the unlock vault popout", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked); activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsSendMessageSpy.mockImplementation();
sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey }); sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey });
await flushPromises(); await flushPromises();
@@ -2443,11 +2686,10 @@ describe("OverlayBackground", () => {
}); });
await flushPromises(); await flushPromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith( expect(tabsSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
sender.tab, command: "addNewVaultItemFromOverlay",
{ command: "addNewVaultItemFromOverlay", addNewCipherType: CipherType.Login }, addNewCipherType: CipherType.Login,
{ frameId: sender.frameId }, });
);
}); });
}); });

View File

@@ -42,6 +42,7 @@ import { generateRandomChars } from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import { import {
CloseInlineMenuMessage, CloseInlineMenuMessage,
CurrentAddNewItemData,
FocusedFieldData, FocusedFieldData,
InlineMenuButtonPortMessageHandlers, InlineMenuButtonPortMessageHandlers,
InlineMenuCipherData, InlineMenuCipherData,
@@ -83,6 +84,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private cancelUpdateInlineMenuPositionSubject = new Subject<void>(); private cancelUpdateInlineMenuPositionSubject = new Subject<void>();
private repositionInlineMenuSubject = new Subject<chrome.runtime.MessageSender>(); private repositionInlineMenuSubject = new Subject<chrome.runtime.MessageSender>();
private rebuildSubFrameOffsetsSubject = new Subject<chrome.runtime.MessageSender>(); private rebuildSubFrameOffsetsSubject = new Subject<chrome.runtime.MessageSender>();
private addNewVaultItemSubject = new Subject<CurrentAddNewItemData>();
private currentAddNewItemData: CurrentAddNewItemData;
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFocused: boolean = false;
private isFieldCurrentlyFilling: boolean = false; private isFieldCurrentlyFilling: boolean = false;
@@ -187,6 +190,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
switchMap((sender) => this.rebuildSubFrameOffsets(sender)), switchMap((sender) => this.rebuildSubFrameOffsets(sender)),
) )
.subscribe(); .subscribe();
this.addNewVaultItemSubject
.pipe(
debounceTime(100),
switchMap((addNewItemData) =>
this.buildCipherAndOpenAddEditVaultItemPopout(addNewItemData),
),
)
.subscribe();
// Debounce used to update inline menu position // Debounce used to update inline menu position
merge( merge(
@@ -231,14 +242,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
if (authStatus !== AuthenticationStatus.Unlocked) { if (authStatus !== AuthenticationStatus.Unlocked) {
if (this.focusedFieldData) { if (this.focusedFieldData) {
void this.closeInlineMenuAfterCiphersUpdate(); this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
} }
return; return;
} }
const currentTab = await BrowserApi.getTabFromCurrentWindowId(); const currentTab = await BrowserApi.getTabFromCurrentWindowId();
if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) { if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) {
void this.closeInlineMenuAfterCiphersUpdate(); this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
} }
this.inlineMenuCiphers = new Map(); this.inlineMenuCiphers = new Map();
@@ -319,7 +330,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private async getInlineMenuCipherData(): Promise<InlineMenuCipherData[]> { private async getInlineMenuCipherData(): Promise<InlineMenuCipherData[]> {
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers); const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers);
let inlineMenuCipherData: InlineMenuCipherData[] = []; let inlineMenuCipherData: InlineMenuCipherData[];
if (this.showInlineMenuAccountCreation()) { if (this.showInlineMenuAccountCreation()) {
inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers( inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers(
@@ -527,10 +538,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}; };
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) { if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url); this.buildSubFrameOffsets(
void BrowserApi.tabSendMessage(pageDetails.tab, { pageDetails.tab,
pageDetails.frameId,
pageDetails.details.url,
).catch((error) => this.logService.error(error));
BrowserApi.tabSendMessage(pageDetails.tab, {
command: "setupRebuildSubFrameOffsetsListeners", command: "setupRebuildSubFrameOffsetsListeners",
}); }).catch((error) => this.logService.error(error));
} }
const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; const pageDetailsMap = this.pageDetailsForTab[sender.tab.id];
@@ -620,11 +635,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (!subFrameOffset) { if (!subFrameOffset) {
subFrameOffsetsForTab.set(frameId, null); subFrameOffsetsForTab.set(frameId, null);
void BrowserApi.tabSendMessage( BrowserApi.tabSendMessage(
tab, tab,
{ command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId }, { command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId },
{ frameId }, { frameId },
); ).catch((error) => this.logService.error(error));
return; return;
} }
@@ -656,11 +671,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
frameId, frameId,
); );
void BrowserApi.tabSendMessage( BrowserApi.tabSendMessage(
tab, tab,
{ command: "destroyAutofillInlineMenuListeners" }, { command: "destroyAutofillInlineMenuListeners" },
{ frameId }, { frameId },
); ).catch((error) => this.logService.error(error));
} }
/** /**
@@ -696,13 +711,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
} }
if (!this.checkIsInlineMenuButtonVisible()) { if (!this.checkIsInlineMenuButtonVisible()) {
void this.toggleInlineMenuHidden( this.toggleInlineMenuHidden(
{ isInlineMenuHidden: false, setTransparentInlineMenu: true }, { isInlineMenuHidden: false, setTransparentInlineMenu: true },
sender, sender,
); ).catch((error) => this.logService.error(error));
} }
void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender); this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender).catch(
(error) => this.logService.error(error),
);
const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage( const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage(
sender.tab, sender.tab,
@@ -722,7 +739,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return; return;
} }
void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender); this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender).catch(
(error) => this.logService.error(error),
);
} }
/** /**
@@ -807,7 +826,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const command = "closeAutofillInlineMenu"; const command = "closeAutofillInlineMenu";
const sendOptions = { frameId: 0 }; const sendOptions = { frameId: 0 };
if (forceCloseInlineMenu) { if (forceCloseInlineMenu) {
void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch(
(error) => this.logService.error(error),
);
this.isInlineMenuButtonVisible = false; this.isInlineMenuButtonVisible = false;
this.isInlineMenuListVisible = false; this.isInlineMenuListVisible = false;
return; return;
@@ -818,11 +839,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
} }
if (this.isFieldCurrentlyFilling) { if (this.isFieldCurrentlyFilling) {
void BrowserApi.tabSendMessage( BrowserApi.tabSendMessage(
sender.tab, sender.tab,
{ command, overlayElement: AutofillOverlayElement.List }, { command, overlayElement: AutofillOverlayElement.List },
sendOptions, sendOptions,
); ).catch((error) => this.logService.error(error));
this.isInlineMenuListVisible = false; this.isInlineMenuListVisible = false;
return; return;
} }
@@ -840,7 +861,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.isInlineMenuListVisible = false; this.isInlineMenuListVisible = false;
} }
void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch((error) =>
this.logService.error(error),
);
} }
/** /**
@@ -1092,11 +1115,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
) { ) {
if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) {
void BrowserApi.tabSendMessage( BrowserApi.tabSendMessage(
sender.tab, sender.tab,
{ command: "unsetMostRecentlyFocusedField" }, { command: "unsetMostRecentlyFocusedField" },
{ frameId: this.focusedFieldData.frameId }, { frameId: this.focusedFieldData.frameId },
); ).catch((error) => this.logService.error(error));
} }
const previousFocusedFieldData = this.focusedFieldData; const previousFocusedFieldData = this.focusedFieldData;
@@ -1108,7 +1131,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
!this.focusedFieldData.showInlineMenuAccountCreation; !this.focusedFieldData.showInlineMenuAccountCreation;
if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) { if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) {
void this.updateIdentityCiphersOnLoginField(previousFocusedFieldData); this.updateIdentityCiphersOnLoginField(previousFocusedFieldData).catch((error) =>
this.logService.error(error),
);
return;
}
if (previousFocusedFieldData?.filledByCipherType !== focusedFieldData?.filledByCipherType) {
const updateAllCipherTypes = focusedFieldData.filledByCipherType !== CipherType.Login;
this.updateOverlayCiphers(updateAllCipherTypes).catch((error) =>
this.logService.error(error),
);
} }
} }
@@ -1355,9 +1388,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return; return;
} }
void BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", { BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", {
direction, direction,
}); }).catch((error) => this.logService.error(error));
} }
/** /**
@@ -1375,13 +1408,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return; return;
} }
void BrowserApi.tabSendMessage( this.currentAddNewItemData = { addNewCipherType, sender };
sender.tab, BrowserApi.tabSendMessage(sender.tab, {
{ command: "addNewVaultItemFromOverlay", addNewCipherType }, command: "addNewVaultItemFromOverlay",
{ addNewCipherType,
frameId: this.focusedFieldData.frameId || 0, }).catch((error) => this.logService.error(error));
},
);
} }
/** /**
@@ -1398,18 +1429,154 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ addNewCipherType, login, card, identity }: OverlayAddNewItemMessage, { addNewCipherType, login, card, identity }: OverlayAddNewItemMessage,
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
) { ) {
if (!addNewCipherType) { if (
!this.currentAddNewItemData ||
sender.tab.id !== this.currentAddNewItemData.sender.tab.id ||
!addNewCipherType ||
this.currentAddNewItemData.addNewCipherType !== addNewCipherType
) {
return; return;
} }
if (login && this.isAddingNewLogin()) {
this.updateCurrentAddNewItemLogin(login);
}
if (card && this.isAddingNewCard()) {
this.updateCurrentAddNewItemCard(card);
}
if (identity && this.isAddingNewIdentity()) {
this.updateCurrentAddNewItemIdentity(identity);
}
this.addNewVaultItemSubject.next(this.currentAddNewItemData);
}
/**
* Identifies if the current add new item data is for adding a new login.
*/
private isAddingNewLogin() {
return this.currentAddNewItemData.addNewCipherType === CipherType.Login;
}
/**
* Identifies if the current add new item data is for adding a new card.
*/
private isAddingNewCard() {
return this.currentAddNewItemData.addNewCipherType === CipherType.Card;
}
/**
* Identifies if the current add new item data is for adding a new identity.
*/
private isAddingNewIdentity() {
return this.currentAddNewItemData.addNewCipherType === CipherType.Identity;
}
/**
* Updates the current add new item data with the provided login data. If the
* login data is already present, the data will be merged with the existing data.
*
* @param login - The login data captured from the extension message
*/
private updateCurrentAddNewItemLogin(login: NewLoginCipherData) {
if (!this.currentAddNewItemData.login) {
this.currentAddNewItemData.login = login;
return;
}
const currentLoginData = this.currentAddNewItemData.login;
this.currentAddNewItemData.login = {
uri: login.uri || currentLoginData.uri,
hostname: login.hostname || currentLoginData.hostname,
username: login.username || currentLoginData.username,
password: login.password || currentLoginData.password,
};
}
/**
* Updates the current add new item data with the provided card data. If the
* card data is already present, the data will be merged with the existing data.
*
* @param card - The card data captured from the extension message
*/
private updateCurrentAddNewItemCard(card: NewCardCipherData) {
if (!this.currentAddNewItemData.card) {
this.currentAddNewItemData.card = card;
return;
}
const currentCardData = this.currentAddNewItemData.card;
this.currentAddNewItemData.card = {
cardholderName: card.cardholderName || currentCardData.cardholderName,
number: card.number || currentCardData.number,
expirationMonth: card.expirationMonth || currentCardData.expirationMonth,
expirationYear: card.expirationYear || currentCardData.expirationYear,
expirationDate: card.expirationDate || currentCardData.expirationDate,
cvv: card.cvv || currentCardData.cvv,
};
}
/**
* Updates the current add new item data with the provided identity data. If the
* identity data is already present, the data will be merged with the existing data.
*
* @param identity - The identity data captured from the extension message
*/
private updateCurrentAddNewItemIdentity(identity: NewIdentityCipherData) {
if (!this.currentAddNewItemData.identity) {
this.currentAddNewItemData.identity = identity;
return;
}
const currentIdentityData = this.currentAddNewItemData.identity;
this.currentAddNewItemData.identity = {
title: identity.title || currentIdentityData.title,
firstName: identity.firstName || currentIdentityData.firstName,
middleName: identity.middleName || currentIdentityData.middleName,
lastName: identity.lastName || currentIdentityData.lastName,
fullName: identity.fullName || currentIdentityData.fullName,
address1: identity.address1 || currentIdentityData.address1,
address2: identity.address2 || currentIdentityData.address2,
address3: identity.address3 || currentIdentityData.address3,
city: identity.city || currentIdentityData.city,
state: identity.state || currentIdentityData.state,
postalCode: identity.postalCode || currentIdentityData.postalCode,
country: identity.country || currentIdentityData.country,
company: identity.company || currentIdentityData.company,
phone: identity.phone || currentIdentityData.phone,
email: identity.email || currentIdentityData.email,
username: identity.username || currentIdentityData.username,
};
}
/**
* Handles building a new cipher and opening the add/edit vault item popout.
*
* @param login - The login data captured from the extension message
* @param card - The card data captured from the extension message
* @param identity - The identity data captured from the extension message
* @param sender - The sender of the extension message
*/
private async buildCipherAndOpenAddEditVaultItemPopout({
login,
card,
identity,
sender,
}: CurrentAddNewItemData) {
const cipherView: CipherView = this.buildNewVaultItemCipherView({ const cipherView: CipherView = this.buildNewVaultItemCipherView({
addNewCipherType,
login, login,
card, card,
identity, identity,
}); });
if (cipherView) { if (!cipherView) {
this.currentAddNewItemData = null;
return;
}
try {
this.closeInlineMenu(sender); this.closeInlineMenu(sender);
await this.cipherService.setAddEditCipherInfo({ await this.cipherService.setAddEditCipherInfo({
cipher: cipherView, cipher: cipherView,
@@ -1418,32 +1585,30 @@ export class OverlayBackground implements OverlayBackgroundInterface {
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
} catch (error) {
this.logService.error("Error building cipher and opening add/edit vault item popout", error);
} }
this.currentAddNewItemData = null;
} }
/** /**
* Builds and returns a new cipher view with the provided vault item data. * Builds and returns a new cipher view with the provided vault item data.
* *
* @param addNewCipherType - The type of cipher to add
* @param login - The login data captured from the extension message * @param login - The login data captured from the extension message
* @param card - The card data captured from the extension message * @param card - The card data captured from the extension message
* @param identity - The identity data captured from the extension message * @param identity - The identity data captured from the extension message
*/ */
private buildNewVaultItemCipherView({ private buildNewVaultItemCipherView({ login, card, identity }: OverlayAddNewItemMessage) {
addNewCipherType, if (login && this.isAddingNewLogin()) {
login,
card,
identity,
}: OverlayAddNewItemMessage) {
if (login && addNewCipherType === CipherType.Login) {
return this.buildLoginCipherView(login); return this.buildLoginCipherView(login);
} }
if (card && addNewCipherType === CipherType.Card) { if (card && this.isAddingNewCard()) {
return this.buildCardCipherView(card); return this.buildCardCipherView(card);
} }
if (identity && addNewCipherType === CipherType.Identity) { if (identity && this.isAddingNewIdentity()) {
return this.buildIdentityCipherView(identity); return this.buildIdentityCipherView(identity);
} }
} }
@@ -1708,7 +1873,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.resetFocusedFieldSubFrameOffsets(sender); this.resetFocusedFieldSubFrameOffsets(sender);
this.cancelInlineMenuFadeInAndPositionUpdate(); this.cancelInlineMenuFadeInAndPositionUpdate();
void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender); this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender).catch((error) =>
this.logService.error(error),
);
this.repositionInlineMenuSubject.next(sender); this.repositionInlineMenuSubject.next(sender);
} }
@@ -1898,14 +2065,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
filledByCipherType: this.focusedFieldData?.filledByCipherType, filledByCipherType: this.focusedFieldData?.filledByCipherType,
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
}); });
void this.updateInlineMenuPosition( this.updateInlineMenuPosition(
{ {
overlayElement: isInlineMenuListPort overlayElement: isInlineMenuListPort
? AutofillOverlayElement.List ? AutofillOverlayElement.List
: AutofillOverlayElement.Button, : AutofillOverlayElement.Button,
}, },
port.sender, port.sender,
); ).catch((error) => this.logService.error(error));
}; };
/** /**

View File

@@ -1099,7 +1099,9 @@ describe("AutofillOverlayContentService", () => {
selectFieldElement.dispatchEvent(new Event("focus")); selectFieldElement.dispatchEvent(new Event("focus"));
await flushPromises(); await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu"); expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
}); });
it("updates the most recently focused field", async () => { it("updates the most recently focused field", async () => {

View File

@@ -249,10 +249,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* to the background script to add a new cipher. * to the background script to add a new cipher.
*/ */
async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) { async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
if (!(await this.isInlineMenuListVisible())) {
return;
}
const command = "autofillOverlayAddNewVaultItem"; const command = "autofillOverlayAddNewVaultItem";
if (addNewCipherType === CipherType.Login) { if (addNewCipherType === CipherType.Login) {
@@ -680,7 +676,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
} }
if (elementIsSelectElement(formFieldElement)) { if (elementIsSelectElement(formFieldElement)) {
await this.sendExtensionMessage("closeAutofillInlineMenu"); await this.sendExtensionMessage("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
return; return;
} }
@@ -763,7 +761,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private async updateMostRecentlyFocusedField( private async updateMostRecentlyFocusedField(
formFieldElement: ElementWithOpId<FormFieldElement>, formFieldElement: ElementWithOpId<FormFieldElement>,
) { ) {
if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) { if (
!formFieldElement ||
!elementIsFillableFormField(formFieldElement) ||
elementIsSelectElement(formFieldElement)
) {
return; return;
} }