[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 482 additions and 65 deletions

View File

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

View File

@ -176,8 +176,12 @@ describe("OverlayBackground", () => {
parentFrameId: getFrameCounter,
});
});
tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage");
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData");
tabsSendMessageSpy = jest
.spyOn(BrowserApi, "tabSendMessage")
.mockImplementation(() => Promise.resolve());
tabSendMessageDataSpy = jest
.spyOn(BrowserApi, "tabSendMessageData")
.mockImplementation(() => Promise.resolve());
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
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 () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@ -857,7 +861,7 @@ describe("OverlayBackground", () => {
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
id: "inline-menu-cipher-1",
id: "inline-menu-cipher-0",
login: {
username: "username-1",
},
@ -1119,10 +1123,12 @@ describe("OverlayBackground", () => {
let openAddEditVaultItemPopoutSpy: jest.SpyInstance;
beforeEach(() => {
jest.useFakeTimers();
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
openAddEditVaultItemPopoutSpy = jest
.spyOn(overlayBackground as any, "openAddEditVaultItemPopout")
.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", () => {
@ -1132,6 +1138,28 @@ describe("OverlayBackground", () => {
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 () => {
sendMockExtensionMessage(
{
@ -1146,6 +1174,7 @@ describe("OverlayBackground", () => {
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@ -1154,6 +1183,8 @@ describe("OverlayBackground", () => {
});
it("creates a new card cipher", async () => {
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card;
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
@ -1169,6 +1200,7 @@ describe("OverlayBackground", () => {
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@ -1177,6 +1209,10 @@ describe("OverlayBackground", () => {
});
describe("creating a new identity cipher", () => {
beforeEach(() => {
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity;
});
it("populates an identity cipher view and creates it", async () => {
sendMockExtensionMessage(
{
@ -1203,6 +1239,7 @@ describe("OverlayBackground", () => {
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@ -1223,6 +1260,7 @@ describe("OverlayBackground", () => {
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@ -1241,6 +1279,7 @@ describe("OverlayBackground", () => {
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
@ -1259,11 +1298,173 @@ describe("OverlayBackground", () => {
},
sender,
);
jest.advanceTimersByTime(100);
await flushPromises();
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", () => {
@ -1363,6 +1564,51 @@ describe("OverlayBackground", () => {
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", () => {
@ -1841,7 +2087,6 @@ describe("OverlayBackground", () => {
overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([
[focusedFieldData.frameId, null],
]);
tabsSendMessageSpy.mockImplementation();
jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent");
sendMockExtensionMessage(
@ -2090,7 +2335,6 @@ describe("OverlayBackground", () => {
describe("autofillInlineMenuButtonClicked message handler", () => {
it("opens the unlock vault popout if the user auth status is not unlocked", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsSendMessageSpy.mockImplementation();
sendPortMessage(buttonMessageConnectorSpy, {
command: "autofillInlineMenuButtonClicked",
@ -2291,7 +2535,6 @@ describe("OverlayBackground", () => {
describe("unlockVault message handler", () => {
it("opens the unlock vault popout", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsSendMessageSpy.mockImplementation();
sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey });
await flushPromises();
@ -2443,11 +2686,10 @@ describe("OverlayBackground", () => {
});
await flushPromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{ command: "addNewVaultItemFromOverlay", addNewCipherType: CipherType.Login },
{ frameId: sender.frameId },
);
expect(tabsSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "addNewVaultItemFromOverlay",
addNewCipherType: CipherType.Login,
});
});
});

View File

@ -42,6 +42,7 @@ import { generateRandomChars } from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import {
CloseInlineMenuMessage,
CurrentAddNewItemData,
FocusedFieldData,
InlineMenuButtonPortMessageHandlers,
InlineMenuCipherData,
@ -83,6 +84,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private cancelUpdateInlineMenuPositionSubject = new Subject<void>();
private repositionInlineMenuSubject = new Subject<chrome.runtime.MessageSender>();
private rebuildSubFrameOffsetsSubject = new Subject<chrome.runtime.MessageSender>();
private addNewVaultItemSubject = new Subject<CurrentAddNewItemData>();
private currentAddNewItemData: CurrentAddNewItemData;
private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean = false;
private isFieldCurrentlyFilling: boolean = false;
@ -187,6 +190,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
switchMap((sender) => this.rebuildSubFrameOffsets(sender)),
)
.subscribe();
this.addNewVaultItemSubject
.pipe(
debounceTime(100),
switchMap((addNewItemData) =>
this.buildCipherAndOpenAddEditVaultItemPopout(addNewItemData),
),
)
.subscribe();
// Debounce used to update inline menu position
merge(
@ -231,14 +242,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
if (authStatus !== AuthenticationStatus.Unlocked) {
if (this.focusedFieldData) {
void this.closeInlineMenuAfterCiphersUpdate();
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
}
return;
}
const currentTab = await BrowserApi.getTabFromCurrentWindowId();
if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) {
void this.closeInlineMenuAfterCiphersUpdate();
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
}
this.inlineMenuCiphers = new Map();
@ -319,7 +330,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private async getInlineMenuCipherData(): Promise<InlineMenuCipherData[]> {
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers);
let inlineMenuCipherData: InlineMenuCipherData[] = [];
let inlineMenuCipherData: InlineMenuCipherData[];
if (this.showInlineMenuAccountCreation()) {
inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers(
@ -527,10 +538,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
};
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url);
void BrowserApi.tabSendMessage(pageDetails.tab, {
this.buildSubFrameOffsets(
pageDetails.tab,
pageDetails.frameId,
pageDetails.details.url,
).catch((error) => this.logService.error(error));
BrowserApi.tabSendMessage(pageDetails.tab, {
command: "setupRebuildSubFrameOffsetsListeners",
});
}).catch((error) => this.logService.error(error));
}
const pageDetailsMap = this.pageDetailsForTab[sender.tab.id];
@ -620,11 +635,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (!subFrameOffset) {
subFrameOffsetsForTab.set(frameId, null);
void BrowserApi.tabSendMessage(
BrowserApi.tabSendMessage(
tab,
{ command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId },
{ frameId },
);
).catch((error) => this.logService.error(error));
return;
}
@ -656,11 +671,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
frameId,
);
void BrowserApi.tabSendMessage(
BrowserApi.tabSendMessage(
tab,
{ command: "destroyAutofillInlineMenuListeners" },
{ frameId },
);
).catch((error) => this.logService.error(error));
}
/**
@ -696,13 +711,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
if (!this.checkIsInlineMenuButtonVisible()) {
void this.toggleInlineMenuHidden(
this.toggleInlineMenuHidden(
{ isInlineMenuHidden: false, setTransparentInlineMenu: true },
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(
sender.tab,
@ -722,7 +739,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
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 sendOptions = { frameId: 0 };
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.isInlineMenuListVisible = false;
return;
@ -818,11 +839,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
if (this.isFieldCurrentlyFilling) {
void BrowserApi.tabSendMessage(
BrowserApi.tabSendMessage(
sender.tab,
{ command, overlayElement: AutofillOverlayElement.List },
sendOptions,
);
).catch((error) => this.logService.error(error));
this.isInlineMenuListVisible = false;
return;
}
@ -840,7 +861,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
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,
) {
if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) {
void BrowserApi.tabSendMessage(
BrowserApi.tabSendMessage(
sender.tab,
{ command: "unsetMostRecentlyFocusedField" },
{ frameId: this.focusedFieldData.frameId },
);
).catch((error) => this.logService.error(error));
}
const previousFocusedFieldData = this.focusedFieldData;
@ -1108,7 +1131,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
!this.focusedFieldData.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;
}
void BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", {
BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", {
direction,
});
}).catch((error) => this.logService.error(error));
}
/**
@ -1375,13 +1408,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
void BrowserApi.tabSendMessage(
sender.tab,
{ command: "addNewVaultItemFromOverlay", addNewCipherType },
{
frameId: this.focusedFieldData.frameId || 0,
},
);
this.currentAddNewItemData = { addNewCipherType, sender };
BrowserApi.tabSendMessage(sender.tab, {
command: "addNewVaultItemFromOverlay",
addNewCipherType,
}).catch((error) => this.logService.error(error));
}
/**
@ -1398,18 +1429,154 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ addNewCipherType, login, card, identity }: OverlayAddNewItemMessage,
sender: chrome.runtime.MessageSender,
) {
if (!addNewCipherType) {
if (
!this.currentAddNewItemData ||
sender.tab.id !== this.currentAddNewItemData.sender.tab.id ||
!addNewCipherType ||
this.currentAddNewItemData.addNewCipherType !== addNewCipherType
) {
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({
addNewCipherType,
login,
card,
identity,
});
if (cipherView) {
if (!cipherView) {
this.currentAddNewItemData = null;
return;
}
try {
this.closeInlineMenu(sender);
await this.cipherService.setAddEditCipherInfo({
cipher: cipherView,
@ -1418,32 +1585,30 @@ export class OverlayBackground implements OverlayBackgroundInterface {
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
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.
*
* @param addNewCipherType - The type of cipher to add
* @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
*/
private buildNewVaultItemCipherView({
addNewCipherType,
login,
card,
identity,
}: OverlayAddNewItemMessage) {
if (login && addNewCipherType === CipherType.Login) {
private buildNewVaultItemCipherView({ login, card, identity }: OverlayAddNewItemMessage) {
if (login && this.isAddingNewLogin()) {
return this.buildLoginCipherView(login);
}
if (card && addNewCipherType === CipherType.Card) {
if (card && this.isAddingNewCard()) {
return this.buildCardCipherView(card);
}
if (identity && addNewCipherType === CipherType.Identity) {
if (identity && this.isAddingNewIdentity()) {
return this.buildIdentityCipherView(identity);
}
}
@ -1708,7 +1873,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.resetFocusedFieldSubFrameOffsets(sender);
this.cancelInlineMenuFadeInAndPositionUpdate();
void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender);
this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender).catch((error) =>
this.logService.error(error),
);
this.repositionInlineMenuSubject.next(sender);
}
@ -1898,14 +2065,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
filledByCipherType: this.focusedFieldData?.filledByCipherType,
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
});
void this.updateInlineMenuPosition(
this.updateInlineMenuPosition(
{
overlayElement: isInlineMenuListPort
? AutofillOverlayElement.List
: AutofillOverlayElement.Button,
},
port.sender,
);
).catch((error) => this.logService.error(error));
};
/**

View File

@ -1099,7 +1099,9 @@ describe("AutofillOverlayContentService", () => {
selectFieldElement.dispatchEvent(new Event("focus"));
await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
});
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.
*/
async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
if (!(await this.isInlineMenuListVisible())) {
return;
}
const command = "autofillOverlayAddNewVaultItem";
if (addNewCipherType === CipherType.Login) {
@ -680,7 +676,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (elementIsSelectElement(formFieldElement)) {
await this.sendExtensionMessage("closeAutofillInlineMenu");
await this.sendExtensionMessage("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
return;
}
@ -763,7 +761,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private async updateMostRecentlyFocusedField(
formFieldElement: ElementWithOpId<FormFieldElement>,
) {
if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) {
if (
!formFieldElement ||
!elementIsFillableFormField(formFieldElement) ||
elementIsSelectElement(formFieldElement)
) {
return;
}