[PM-8322] Firefox Inline Autofill Menu Not Propagating Correctly When Switching Tabs (#9300)

* [PM-8322] Firefox Inline Autofill Menu Not Propagation Correctly When Switching Tabs

* [PM-8322] Firefox Inline Autofill Menu Not Propagation Correctly When Switching Tabs

* Add missing RouterModule to the CurrentAccountComponent (#9295)

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [PM-6825] Browser Refresh - Initial List Items (#9199)

* [PM-6825] Add temporary vault page header

* [PM-6825] Expose cipherViews$ observable

* [PM-6825] Refactor getAllDecryptedForUrl to expose filter functionality for reuse

* [PM-6825] Introduce VaultPopupItemsService

* [PM-6825] Introduce initial VaultListItem and VaultListItemsContainer components

* [PM-6825] Add VaultListItems to VaultV2 component

* [PM-6825] Introduce autofill-vault-list-items.component to encapsulate autofill logic

* [PM-6825] Add temporary Vault icon

* [PM-6825] Add empty and no results states to Vault tab

* [PM-6825] Add unit tests for vault popup items service

* [PM-6825] Negate noFilteredResults placeholder

* [PM-6825] Cleanup new Vault components

* [PM-6825] Move new components into its own module

* [PM-6825] Fix missing button type

* [PM-6825] Add booleanAttribute to showAutofill input

* [PM-6825] Replace empty refresh BehaviorSubject with Subject

* [PM-6825] Combine *ngIfs for vault list items container

* [PM-6825] Use popup-section-header component

* [PM-6825] Use small variant for icon buttons

* [PM-6825] Use anchor tag for vault items

* [PM-6825] Consolidate vault-list-items-container to include list item component functionality directly

* [PM-6825] Add Tailwind classes to new Vault icon

* [PM-6825] Remove temporary header comment

* [PM-6825] Fix auto fill suggestion font size and padding

* [PM-6825] Use tailwind for vault icon styling

* [PM-6825] Add libs/angular to tailwind.config content

* [PM-6825] Cleanup missing i18n

* [PM-6825] Make VaultV2 standalone and cleanup Browser App module

* [PM-6825] Use explicit type annotation

* [PM-6825] Use property binding instead of interpolation

* [PM-7076] Create settings-v2.component (#9213)

* Create settings-v2.component

Create new settings page
Add routing based on extension refresh flag

* Wrap anchors around the icons

* Add account-switcher to settings page

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [PM-8289] Inline menu content script does not update when user updates setting (#9279)

* [PM-8289] Inline menu content script does not update whne user updates setting

* [PM-8289] Fixing issue present within Jest tests

* [PM-8289] Triggering a reload of autofill scripts when a user logs into their account

---------

Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
Cesar Gonzalez 2024-05-29 11:35:17 -05:00 committed by GitHub
parent a6df923416
commit a7aaa140fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 136 additions and 39 deletions

View File

@ -42,6 +42,7 @@ type OverlayPortMessage = {
type FocusedFieldData = {
focusedFieldStyles: Partial<CSSStyleDeclaration>;
focusedFieldRects: Partial<DOMRect>;
tabId?: number;
};
type OverlayCipherData = {
@ -66,14 +67,14 @@ type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSende
type OverlayBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction;
openAutofillOverlay: () => void;
autofillOverlayElementClosed: ({ message }: BackgroundMessageParam) => void;
autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
getAutofillOverlayVisibility: () => void;
checkAutofillOverlayFocused: () => void;
focusAutofillOverlayList: () => void;
updateAutofillOverlayPosition: ({ message }: BackgroundMessageParam) => void;
updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void;
updateFocusedFieldData: ({ message }: BackgroundMessageParam) => void;
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
unlockCompleted: ({ message }: BackgroundMessageParam) => void;
addEditCipherSubmitted: () => void;

View File

@ -517,7 +517,7 @@ describe("OverlayBackground", () => {
expect(returnValue).toBe(undefined);
expect(sendResponse).not.toHaveBeenCalled();
expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message);
expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender);
});
it("will return a response if the message handler returns a response", async () => {
@ -570,6 +570,26 @@ describe("OverlayBackground", () => {
await initOverlayElementPorts();
});
it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => {
const port1 = mock<chrome.runtime.Port>();
const port2 = mock<chrome.runtime.Port>();
overlayBackground["expiredPorts"] = [port1, port2];
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
sendExtensionRuntimeMessage(
{
command: "autofillOverlayElementClosed",
overlayElement: AutofillOverlayElement.Button,
},
sender,
);
expect(port1.disconnect).toHaveBeenCalled();
expect(port2.disconnect).toHaveBeenCalled();
});
it("disconnects the button element port", () => {
sendExtensionRuntimeMessage({
command: "autofillOverlayElementClosed",
@ -729,6 +749,23 @@ describe("OverlayBackground", () => {
});
});
it("skips updating the position if the most recently focused field is different than the message sender", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
sendExtensionRuntimeMessage({ command: "updateAutofillOverlayPosition" }, sender);
expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
command: "updateIframePosition",
styles: expect.anything(),
});
expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({
command: "updateIframePosition",
styles: expect.anything(),
});
});
it("updates the overlay button's position", () => {
const focusedFieldData = createFocusedFieldDataMock();
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
@ -796,12 +833,14 @@ describe("OverlayBackground", () => {
});
it("will post a message to the overlay list facilitating an update of the list's position", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock();
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
overlayBackground["updateOverlayPosition"]({
overlayElement: AutofillOverlayElement.List,
});
overlayBackground["updateOverlayPosition"](
{ overlayElement: AutofillOverlayElement.List },
sender,
);
sendExtensionRuntimeMessage({
command: "updateAutofillOverlayPosition",
overlayElement: AutofillOverlayElement.List,
@ -1017,9 +1056,10 @@ describe("OverlayBackground", () => {
expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css");
expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled();
expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith({
overlayElement: AutofillOverlayElement.List,
});
expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith(
{ overlayElement: AutofillOverlayElement.List },
listPortSpy.sender,
);
});
it("sets up the overlay button port if the port connection is for the overlay button", async () => {
@ -1032,9 +1072,19 @@ describe("OverlayBackground", () => {
expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled();
expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css");
expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith({
overlayElement: AutofillOverlayElement.Button,
});
expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith(
{ overlayElement: AutofillOverlayElement.Button },
buttonPortSpy.sender,
);
});
it("stores an existing overlay port so that it can be disconnected at a later time", async () => {
overlayBackground["overlayButtonPort"] = mock<chrome.runtime.Port>();
await initOverlayElementPorts({ initList: false, initButton: true });
await flushPromises();
expect(overlayBackground["expiredPorts"].length).toBe(1);
});
it("gets the system theme", async () => {

View File

@ -54,19 +54,22 @@ class OverlayBackground implements OverlayBackgroundInterface {
private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
private overlayButtonPort: chrome.runtime.Port;
private overlayListPort: chrome.runtime.Port;
private expiredPorts: chrome.runtime.Port[] = [];
private focusedFieldData: FocusedFieldData;
private overlayPageTranslations: Record<string, string>;
private iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
openAutofillOverlay: () => this.openOverlay(false),
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
autofillOverlayElementClosed: ({ message, sender }) =>
this.overlayElementClosed(message, sender),
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
getAutofillOverlayVisibility: () => this.getOverlayVisibility(),
checkAutofillOverlayFocused: () => this.checkOverlayFocused(),
focusAutofillOverlayList: () => this.focusOverlayList(),
updateAutofillOverlayPosition: ({ message }) => this.updateOverlayPosition(message),
updateAutofillOverlayPosition: ({ message, sender }) =>
this.updateOverlayPosition(message, sender),
updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message),
updateFocusedFieldData: ({ message }) => this.setFocusedFieldData(message),
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message),
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
@ -302,8 +305,18 @@ class OverlayBackground implements OverlayBackgroundInterface {
* the list and button ports and sets them to null.
*
* @param overlayElement - The overlay element that was closed, either the list or button
* @param sender - The sender of the port message
*/
private overlayElementClosed({ overlayElement }: OverlayBackgroundExtensionMessage) {
private overlayElementClosed(
{ overlayElement }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
if (sender.tab.id !== this.focusedFieldData?.tabId) {
this.expiredPorts.forEach((port) => port.disconnect());
this.expiredPorts = [];
return;
}
if (overlayElement === AutofillOverlayElement.Button) {
this.overlayButtonPort?.disconnect();
this.overlayButtonPort = null;
@ -320,9 +333,13 @@ class OverlayBackground implements OverlayBackgroundInterface {
* is based on the focused field's position and dimensions.
*
* @param overlayElement - The overlay element to update, either the list or button
* @param sender - The sender of the port message
*/
private updateOverlayPosition({ overlayElement }: { overlayElement?: string }) {
if (!overlayElement) {
private updateOverlayPosition(
{ overlayElement }: { overlayElement?: string },
sender: chrome.runtime.MessageSender,
) {
if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) {
return;
}
@ -396,9 +413,13 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Sets the focused field data to the data passed in the extension message.
*
* @param focusedFieldData - Contains the rects and styles of the focused field.
* @param sender - The sender of the extension message
*/
private setFocusedFieldData({ focusedFieldData }: OverlayBackgroundExtensionMessage) {
this.focusedFieldData = focusedFieldData;
private setFocusedFieldData(
{ focusedFieldData }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id };
}
/**
@ -690,17 +711,11 @@ class OverlayBackground implements OverlayBackgroundInterface {
private handlePortOnConnect = async (port: chrome.runtime.Port) => {
const isOverlayListPort = port.name === AutofillOverlayPort.List;
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
if (!isOverlayListPort && !isOverlayButtonPort) {
return;
}
if (isOverlayListPort) {
this.overlayListPort = port;
} else {
this.overlayButtonPort = port;
}
this.storeOverlayPort(port);
port.onMessage.addListener(this.handleOverlayElementPortMessage);
port.postMessage({
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
@ -710,13 +725,47 @@ class OverlayBackground implements OverlayBackgroundInterface {
translations: this.getTranslations(),
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
});
this.updateOverlayPosition({
overlayElement: isOverlayListPort
? AutofillOverlayElement.List
: AutofillOverlayElement.Button,
});
this.updateOverlayPosition(
{
overlayElement: isOverlayListPort
? AutofillOverlayElement.List
: AutofillOverlayElement.Button,
},
port.sender,
);
};
/**
* Stores the connected overlay port and sets up any existing ports to be disconnected.
*
* @param port - The port to store
| */
private storeOverlayPort(port: chrome.runtime.Port) {
if (port.name === AutofillOverlayPort.List) {
this.storeExpiredOverlayPort(this.overlayListPort);
this.overlayListPort = port;
return;
}
if (port.name === AutofillOverlayPort.Button) {
this.storeExpiredOverlayPort(this.overlayButtonPort);
this.overlayButtonPort = port;
}
}
/**
* When registering a new connection, we want to ensure that the port is disconnected.
* This method places an existing port in the expiredPorts array to be disconnected
* at a later time.
*
* @param port - The port to store in the expiredPorts array
*/
private storeExpiredOverlayPort(port: chrome.runtime.Port | null) {
if (port) {
this.expiredPorts.push(port);
}
}
/**
* Handles messages sent to the overlay list or button ports.
*

View File

@ -186,9 +186,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.overlayButtonElement.remove();
this.isOverlayButtonVisible = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayElementClosed", {
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button,
});
this.removeOverlayRepositionEventListeners();
@ -204,9 +202,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.overlayListElement.remove();
this.isOverlayListVisible = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayElementClosed", {
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List,
});
}

View File

@ -249,6 +249,7 @@ function createFocusedFieldDataMock(customFields = {}) {
paddingRight: "6px",
paddingLeft: "6px",
},
tabId: 1,
...customFields,
};
}