1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-09 19:28:06 +01:00

[PM-5189] Working through further issues on positioning of inline menu

This commit is contained in:
Cesar Gonzalez 2024-06-25 09:14:02 -05:00
parent 582cdf17dd
commit 4114b538e5
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
8 changed files with 173 additions and 232 deletions

View File

@ -65,6 +65,7 @@ export type OverlayBackgroundExtensionMessage = {
details?: AutofillPageDetails;
isFieldCurrentlyFocused?: boolean;
isFieldCurrentlyFilling?: boolean;
isInlineMenuElementVisible?: boolean;
subFrameData?: SubFrameOffsetData;
focusedFieldData?: FocusedFieldData;
styles?: Partial<CSSStyleDeclaration>;
@ -119,9 +120,12 @@ export type OverlayBackgroundExtensionMessageHandlers = {
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
toggleAutofillInlineMenuHidden: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
checkIsAutofillInlineMenuButtonVisible: ({ sender }: BackgroundSenderParam) => void;
checkIsAutofillInlineMenuListVisible: ({ sender }: BackgroundSenderParam) => void;
updateAutofillInlineMenuElementIsVisibleStatus: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => void;
checkIsAutofillInlineMenuButtonVisible: () => void;
checkIsAutofillInlineMenuListVisible: () => void;
getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number;
updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void;

View File

@ -519,9 +519,10 @@ describe("OverlayBackground", () => {
await flushPromises();
}
beforeEach(() => {
beforeEach(async () => {
sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
jest.useFakeTimers();
await initOverlayElementPorts();
});
it("skips updating the position of either inline menu element if a field is not currently focused", async () => {
@ -559,11 +560,10 @@ describe("OverlayBackground", () => {
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
await flushUpdateInlineMenuPromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{ command: "toggleAutofillInlineMenuHidden", isInlineMenuHidden: true },
{ frameId: 0 },
);
expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
command: "toggleAutofillInlineMenuHidden",
styles: { display: "none" },
});
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{
@ -1312,73 +1312,35 @@ describe("OverlayBackground", () => {
});
});
describe("toggleAutofillInlineMenuHidden message handler", () => {
beforeEach(async () => {
await initOverlayElementPorts();
});
it("returns early if the sender tab is not equal to the focused field tab", async () => {
describe("checkIsAutofillInlineMenuButtonVisible message handler", () => {
it("returns true when the inline menu button is visible", async () => {
overlayBackground["isInlineMenuButtonVisible"] = true;
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
sendMockExtensionMessage({ command: "toggleAutofillInlineMenuHidden" }, sender);
expect(tabsSendMessageSpy).not.toHaveBeenCalled();
});
it("posts a message to the overlay button and list which hides the menu", async () => {
const message = {
command: "toggleAutofillInlineMenuHidden",
isInlineMenuHidden: true,
setTransparentInlineMenu: false,
};
sendMockExtensionMessage(message);
sendMockExtensionMessage(
{ command: "checkIsAutofillInlineMenuButtonVisible" },
sender,
sendResponse,
);
await flushPromises();
expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
command: "toggleAutofillInlineMenuHidden",
styles: {
display: "none",
opacity: "1",
},
});
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "toggleAutofillInlineMenuHidden",
styles: {
display: "none",
opacity: "1",
},
});
});
});
describe("checkIsAutofillInlineMenuButtonVisible", () => {
it("sends a message to the top frame of the tab to identify if the inline menu button is visible", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
sendMockExtensionMessage({ command: "checkIsAutofillInlineMenuButtonVisible" }, sender);
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{ command: "checkIsAutofillInlineMenuButtonVisible" },
{ frameId: 0 },
);
expect(sendResponse).toHaveBeenCalledWith(true);
});
});
describe("checkIsAutofillInlineMenuListVisible message handler", () => {
it("sends a message to the top frame of the tab to identify if the inline menu list is visible", () => {
it("returns true when the inline menu list is visible", async () => {
overlayBackground["isInlineMenuListVisible"] = true;
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
sendMockExtensionMessage({ command: "checkIsAutofillInlineMenuListVisible" }, sender);
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
sendMockExtensionMessage(
{ command: "checkIsAutofillInlineMenuListVisible" },
{ frameId: 0 },
sender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(true);
});
});

View File

@ -74,6 +74,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean = false;
private isFieldCurrentlyFilling: boolean = false;
private isInlineMenuButtonVisible: boolean = false;
private isInlineMenuListVisible: boolean = false;
private iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
autofillOverlayElementClosed: ({ message, sender }) =>
@ -94,11 +96,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
focusAutofillInlineMenuList: () => this.focusInlineMenuList(),
updateAutofillInlineMenuPosition: ({ message, sender }) =>
this.updateInlineMenuPosition(message, sender),
toggleAutofillInlineMenuHidden: ({ message, sender }) =>
this.toggleInlineMenuHidden(message, sender),
checkIsAutofillInlineMenuButtonVisible: ({ sender }) =>
this.checkIsInlineMenuButtonVisible(sender),
checkIsAutofillInlineMenuListVisible: ({ sender }) => this.checkIsInlineMenuListVisible(sender),
updateAutofillInlineMenuElementIsVisibleStatus: ({ message, sender }) =>
this.updateInlineMenuElementIsVisibleStatus(message, sender),
checkIsAutofillInlineMenuButtonVisible: () => this.checkIsInlineMenuButtonVisible(),
checkIsAutofillInlineMenuListVisible: () => this.checkIsInlineMenuListVisible(),
getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender),
updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender),
triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender),
@ -453,7 +454,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
if (!(await this.checkIsInlineMenuButtonVisible(sender))) {
if (!this.checkIsInlineMenuButtonVisible()) {
void this.toggleInlineMenuHidden(
{ isInlineMenuHidden: false, setTransparentInlineMenu: true },
sender,
@ -519,7 +520,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* if it is open, otherwise it will check the inline menu button.
*/
private checkInlineMenuFocused(sender: chrome.runtime.MessageSender) {
if (sender.tab.id !== this.focusedFieldData?.tabId) {
if (!this.senderTabHasFocusedField(sender)) {
return;
}
@ -561,6 +562,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const sendOptions = { frameId: 0 };
if (forceCloseInlineMenu) {
void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions);
this.isInlineMenuButtonVisible = false;
this.isInlineMenuListVisible = false;
return;
}
@ -574,9 +577,18 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ command, overlayElement: AutofillOverlayElement.List },
sendOptions,
);
this.isInlineMenuListVisible = false;
return;
}
if (overlayElement === AutofillOverlayElement.Button) {
this.isInlineMenuButtonVisible = false;
}
if (overlayElement === AutofillOverlayElement.List) {
this.isInlineMenuListVisible = false;
}
void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions);
}
@ -619,21 +631,24 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ overlayElement }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
if (sender.tab.id !== this.focusedFieldData?.tabId) {
if (!this.senderTabHasFocusedField(sender)) {
this.expiredPorts.forEach((port) => port.disconnect());
this.expiredPorts = [];
return;
}
if (overlayElement === AutofillOverlayElement.Button) {
this.inlineMenuButtonPort?.disconnect();
this.inlineMenuButtonPort = null;
this.isInlineMenuButtonVisible = false;
return;
}
this.inlineMenuListPort?.disconnect();
this.inlineMenuListPort = null;
this.isInlineMenuListVisible = false;
}
/**
@ -647,7 +662,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ overlayElement }: { overlayElement?: string },
sender: chrome.runtime.MessageSender,
) {
if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) {
if (!overlayElement || !this.senderTabHasFocusedField(sender)) {
return;
}
@ -686,6 +701,32 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.startInlineMenuFadeIn();
}
/**
* Triggers an update of the inline menu's visibility after the top level frame
* appends the element to the DOM.
*
* @param message - The message received from the content script
* @param sender - The sender of the port message
*/
private updateInlineMenuElementIsVisibleStatus(
message: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
if (!this.senderTabHasFocusedField(sender)) {
return;
}
const { overlayElement, isInlineMenuElementVisible } = message;
if (overlayElement === AutofillOverlayElement.Button) {
this.isInlineMenuButtonVisible = isInlineMenuElementVisible;
return;
}
if (overlayElement === AutofillOverlayElement.List) {
this.isInlineMenuListVisible = isInlineMenuElementVisible;
}
}
/**
* Handles updating the opacity of both the inline menu button and list.
* This is used to simultaneously fade in the inline menu elements.
@ -797,7 +838,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ isInlineMenuHidden, setTransparentInlineMenu }: ToggleInlineMenuHiddenMessage,
sender: chrome.runtime.MessageSender,
) {
if (sender.tab.id !== this.focusedFieldData?.tabId) {
if (!this.senderTabHasFocusedField(sender)) {
return;
}
@ -810,15 +851,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
styles = { ...styles, opacity };
}
await BrowserApi.tabSendMessage(
sender.tab,
{ command: "toggleAutofillInlineMenuHidden", isInlineMenuHidden },
{ frameId: 0 },
);
const portMessage = { command: "toggleAutofillInlineMenuHidden", styles };
this.inlineMenuButtonPort?.postMessage(portMessage);
this.inlineMenuListPort?.postMessage(portMessage);
if (this.inlineMenuButtonPort) {
this.isInlineMenuButtonVisible = !isInlineMenuHidden;
this.inlineMenuButtonPort.postMessage(portMessage);
}
if (this.inlineMenuListPort) {
this.isInlineMenuListVisible = !isInlineMenuHidden;
this.inlineMenuListPort.postMessage(portMessage);
}
if (setTransparentInlineMenu) {
this.startInlineMenuFadeIn();
@ -1010,7 +1052,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param sender - The sender of the port message
*/
private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
if (sender.tab.id !== this.focusedFieldData.tabId) {
if (!this.senderTabHasFocusedField(sender)) {
return;
}
@ -1095,33 +1137,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
/**
* Sends a message to the top level frame of the sender to check if the inline menu button is visible.
*
* @param sender - The sender of the message
* Returns the visibility status of the inline menu button.
*/
private async checkIsInlineMenuButtonVisible(
sender: chrome.runtime.MessageSender,
): Promise<boolean> {
return await BrowserApi.tabSendMessage(
sender.tab,
{ command: "checkIsAutofillInlineMenuButtonVisible" },
{ frameId: 0 },
);
private checkIsInlineMenuButtonVisible(): boolean {
return this.isInlineMenuButtonVisible;
}
/**
* Sends a message to the top level frame of the sender to check if the inline menu list is visible.
*
* @param sender - The sender of the message
* Returns the visibility status of the inline menu list.
*/
private async checkIsInlineMenuListVisible(
sender: chrome.runtime.MessageSender,
): Promise<boolean> {
return await BrowserApi.tabSendMessage(
sender.tab,
{ command: "checkIsAutofillInlineMenuListVisible" },
{ frameId: 0 },
);
private checkIsInlineMenuListVisible(): boolean {
return this.isInlineMenuListVisible;
}
/**
@ -1132,7 +1158,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param sender - The sender of the message
*/
private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) {
return sender.tab.id === this.focusedFieldData.tabId && this.inlineMenuCiphers.size > 0;
return this.senderTabHasFocusedField(sender) && this.inlineMenuCiphers.size > 0;
}
/**
@ -1166,7 +1192,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param sender - The sender of the message
*/
private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean {
if (!this.focusedFieldData || sender.tab.id !== this.focusedFieldData.tabId) {
if (!this.focusedFieldData || !this.senderTabHasFocusedField(sender)) {
return false;
}
@ -1186,6 +1212,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return false;
}
/**
* Identifies if the sender tab is the same as the focused field's tab.
*
* @param sender - The sender of the message
*/
private senderTabHasFocusedField(sender: chrome.runtime.MessageSender) {
return sender.tab.id === this.focusedFieldData?.tabId;
}
/**
* Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu
* if the focused field is within the viewport.
@ -1448,10 +1483,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private handlePortOnDisconnect = (port: chrome.runtime.Port) => {
if (port.name === AutofillOverlayPort.List) {
this.inlineMenuListPort = null;
this.isInlineMenuListVisible = false;
}
if (port.name === AutofillOverlayPort.Button) {
this.inlineMenuButtonPort = null;
this.isInlineMenuButtonVisible = false;
}
};
}

View File

@ -1,5 +1,6 @@
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
import AutofillScript from "../../models/autofill-script";
export type AutofillExtensionMessage = {
@ -13,7 +14,7 @@ export type AutofillExtensionMessage = {
pageDetailsUrl?: string;
ciphers?: any;
isInlineMenuHidden?: boolean;
overlayElement?: string;
overlayElement?: AutofillOverlayElementType;
isFocusingFieldElement?: boolean;
authStatus?: AuthenticationStatus;
isOpeningFullInlineMenu?: boolean;

View File

@ -3,6 +3,9 @@ export const AutofillOverlayElement = {
List: "autofill-inline-menu-list",
} as const;
export type AutofillOverlayElementType =
(typeof AutofillOverlayElement)[keyof typeof AutofillOverlayElement];
export const AutofillOverlayPort = {
Button: "autofill-inline-menu-button-port",
ButtonMessageConnector: "autofill-inline-menu-button-message-connector",

View File

@ -4,9 +4,6 @@ export type InlineMenuExtensionMessageHandlers = {
[key: string]: CallableFunction;
closeAutofillInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
appendAutofillInlineMenuToDom: ({ message }: AutofillExtensionMessageParam) => Promise<void>;
toggleAutofillInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void;
checkIsAutofillInlineMenuButtonVisible: () => boolean;
checkIsAutofillInlineMenuListVisible: () => boolean;
};
export interface AutofillInlineMenuContentService {

View File

@ -1,5 +1,3 @@
import { mock } from "jest-mock-extended";
import AutofillInit from "../../../content/autofill-init";
import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum";
import { createMutationRecordMock } from "../../../spec/autofill-mocks";
@ -13,7 +11,6 @@ describe("AutofillInlineMenuContentService", () => {
let autofillInit: AutofillInit;
let sendExtensionMessageSpy: jest.SpyInstance;
let observeBodyMutationsSpy: jest.SpyInstance;
const sendResponseSpy = jest.fn();
beforeEach(() => {
globalThis.document.body.innerHTML = "";
@ -141,76 +138,6 @@ describe("AutofillInlineMenuContentService", () => {
});
});
});
describe("toggleAutofillInlineMenuHidden message handler", () => {
it("sets the inline elements as hidden if the elements do not exist", () => {
sendMockExtensionMessage({
command: "toggleAutofillInlineMenuHidden",
isInlineMenuHidden: false,
});
expect(autofillInlineMenuContentService["isButtonVisible"]).toBe(false);
expect(autofillInlineMenuContentService["isListVisible"]).toBe(false);
});
it("sets the inline elements as visible", () => {
autofillInlineMenuContentService["buttonElement"] = document.createElement("div");
autofillInlineMenuContentService["listElement"] = document.createElement("div");
sendMockExtensionMessage({
command: "toggleAutofillInlineMenuHidden",
isInlineMenuHidden: false,
});
expect(autofillInlineMenuContentService["isButtonVisible"]).toBe(true);
expect(autofillInlineMenuContentService["isListVisible"]).toBe(true);
});
it("sets the inline elements as hidden", () => {
autofillInlineMenuContentService["buttonElement"] = document.createElement("div");
autofillInlineMenuContentService["listElement"] = document.createElement("div");
autofillInlineMenuContentService["isButtonVisible"] = true;
autofillInlineMenuContentService["isListVisible"] = true;
sendMockExtensionMessage({
command: "toggleAutofillInlineMenuHidden",
isInlineMenuHidden: true,
});
expect(autofillInlineMenuContentService["isButtonVisible"]).toBe(false);
expect(autofillInlineMenuContentService["isListVisible"]).toBe(false);
});
});
describe("checkIsAutofillInlineMenuButtonVisible message handler", () => {
it("returns true if the inline menu button is visible", async () => {
autofillInlineMenuContentService["isButtonVisible"] = true;
sendMockExtensionMessage(
{ command: "checkIsAutofillInlineMenuButtonVisible" },
mock<chrome.runtime.MessageSender>(),
sendResponseSpy,
);
await flushPromises();
expect(sendResponseSpy).toHaveBeenCalledWith(true);
});
});
describe("checkIsAutofillInlineMenuListVisible message handler", () => {
it("returns true if the inline menu list is visible", async () => {
autofillInlineMenuContentService["isListVisible"] = true;
sendMockExtensionMessage(
{ command: "checkIsAutofillInlineMenuListVisible" },
mock<chrome.runtime.MessageSender>(),
sendResponseSpy,
);
await flushPromises();
expect(sendResponseSpy).toHaveBeenCalledWith(true);
});
});
});
describe("handleInlineMenuElementMutationObserverUpdate", () => {
@ -295,6 +222,7 @@ describe("AutofillInlineMenuContentService", () => {
describe("handleBodyElementMutationObserverUpdate", () => {
let buttonElement: HTMLElement;
let listElement: HTMLElement;
let isInlineMenuListVisibleSpy: jest.SpyInstance;
beforeEach(() => {
document.body.innerHTML = `
@ -305,7 +233,9 @@ describe("AutofillInlineMenuContentService", () => {
listElement = document.querySelector(".overlay-list") as HTMLElement;
autofillInlineMenuContentService["buttonElement"] = buttonElement;
autofillInlineMenuContentService["listElement"] = listElement;
autofillInlineMenuContentService["isListVisible"] = true;
isInlineMenuListVisibleSpy = jest
.spyOn(autofillInlineMenuContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
jest.spyOn(globalThis.document.body, "insertBefore");
jest
.spyOn(
@ -315,16 +245,16 @@ describe("AutofillInlineMenuContentService", () => {
.mockReturnValue(false);
});
it("skips handling the mutation if the overlay elements are not present in the DOM", () => {
it("skips handling the mutation if the overlay elements are not present in the DOM", async () => {
autofillInlineMenuContentService["buttonElement"] = undefined;
autofillInlineMenuContentService["listElement"] = undefined;
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
});
it("skips handling the mutation if excessive mutations are being triggered", () => {
it("skips handling the mutation if excessive mutations are being triggered", async () => {
jest
.spyOn(
autofillInlineMenuContentService as any,
@ -332,31 +262,31 @@ describe("AutofillInlineMenuContentService", () => {
)
.mockReturnValue(true);
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
});
it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", () => {
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", async () => {
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
});
it("skips re-arranging the DOM elements if the last child is the overlay button and the overlay list is not visible", () => {
it("skips re-arranging the DOM elements if the last child is the overlay button and the overlay list is not visible", async () => {
listElement.remove();
autofillInlineMenuContentService["isListVisible"] = false;
isInlineMenuListVisibleSpy.mockResolvedValue(false);
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
});
it("positions the overlay button before the overlay list if an element has inserted itself after the button element", () => {
it("positions the overlay button before the overlay list if an element has inserted itself after the button element", async () => {
const injectedElement = document.createElement("div");
document.body.insertBefore(injectedElement, listElement);
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
buttonElement,
@ -364,10 +294,10 @@ describe("AutofillInlineMenuContentService", () => {
);
});
it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", () => {
it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", async () => {
document.body.appendChild(buttonElement);
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
buttonElement,
@ -375,11 +305,11 @@ describe("AutofillInlineMenuContentService", () => {
);
});
it("positions the last child before the overlay button if it is not the overlay list", () => {
it("positions the last child before the overlay button if it is not the overlay list", async () => {
const injectedElement = document.createElement("div");
document.body.appendChild(injectedElement);
autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
injectedElement,

View File

@ -1,5 +1,8 @@
import { AutofillExtensionMessage } from "../../../content/abstractions/autofill-init";
import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum";
import {
AutofillOverlayElement,
AutofillOverlayElementType,
} from "../../../enums/autofill-overlay.enum";
import {
sendExtensionMessage,
generateRandomCustomElementName,
@ -21,8 +24,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
private buttonElement: HTMLElement;
private listElement: HTMLElement;
private isButtonVisible = false;
private isListVisible = false;
private inlineMenuElementsMutationObserver: MutationObserver;
private bodyElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0;
@ -36,9 +37,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private readonly extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
closeAutofillInlineMenu: ({ message }) => this.closeInlineMenu(message),
appendAutofillInlineMenuToDom: ({ message }) => this.appendInlineMenuElements(message),
toggleAutofillInlineMenuHidden: ({ message }) => this.toggleInlineMenuHidden(message),
checkIsAutofillInlineMenuButtonVisible: () => this.isInlineMenuButtonVisible(),
checkIsAutofillInlineMenuListVisible: () => this.isInlineMenuListVisible(),
};
constructor() {
@ -62,28 +60,23 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}
/**
* Identifies if the inline menu button is currently visible.
* Checks if the inline menu button is visible at the top frame.
*/
private isInlineMenuButtonVisible() {
return this.isButtonVisible;
private async isInlineMenuButtonVisible() {
return (
!!this.buttonElement &&
(await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true
);
}
/**
* Identifies if the inline menu list is currently visible.
* Checks if the inline menu list if visible at the top frame.
*/
private isInlineMenuListVisible() {
return this.isListVisible;
}
/**
* Sends a message that facilitates hiding the inline menu elements.
*
* @param message - The message that contains the visibility state of the inline menu elements.
*/
private toggleInlineMenuHidden(message: AutofillExtensionMessage) {
const { isInlineMenuHidden } = message;
this.isButtonVisible = !!this.buttonElement && !isInlineMenuHidden;
this.isListVisible = !!this.listElement && !isInlineMenuHidden;
private async isInlineMenuListVisible() {
return (
!!this.listElement &&
(await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true
);
}
/**
@ -114,7 +107,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private closeInlineMenuButton() {
if (this.buttonElement) {
this.buttonElement.remove();
this.isButtonVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button,
});
@ -127,7 +119,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private closeInlineMenuList() {
if (this.listElement) {
this.listElement.remove();
this.isListVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List,
});
@ -154,9 +145,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
this.updateCustomElementDefaultStyles(this.buttonElement);
}
if (!this.isButtonVisible) {
if (!(await this.isInlineMenuButtonVisible())) {
this.appendInlineMenuElementToBody(this.buttonElement);
this.isButtonVisible = true;
this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.Button, true);
}
}
@ -169,12 +160,28 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
this.updateCustomElementDefaultStyles(this.listElement);
}
if (!this.isListVisible) {
if (!(await this.isInlineMenuListVisible())) {
this.appendInlineMenuElementToBody(this.listElement);
this.isListVisible = true;
this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.List, true);
}
}
/**
* Updates the visibility status of the inline menu element within the background script.
*
* @param overlayElement - The inline menu element to update the visibility status for.
* @param isInlineMenuElementVisible - The visibility status to update the inline menu element to.
*/
private updateInlineMenuElementIsVisibleStatus(
overlayElement: AutofillOverlayElementType,
isInlineMenuElementVisible: boolean,
) {
void this.sendExtensionMessage("updateAutofillInlineMenuElementIsVisibleStatus", {
overlayElement,
isInlineMenuElementVisible,
});
}
/**
* Appends the inline menu element to the body element. This method will also
* observe the body element to ensure that the inline menu element is not
@ -360,7 +367,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* ensure that the inline menu elements are always present at the bottom of the
* body element.
*/
private handleBodyElementMutationObserverUpdate = () => {
private handleBodyElementMutationObserverUpdate = async () => {
if (
(!this.buttonElement && !this.listElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
@ -377,14 +384,14 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
if (
!lastChild ||
(lastChildIsInlineMenuList && secondToLastChildIsInlineMenuButton) ||
(lastChildIsInlineMenuButton && !this.isListVisible)
(lastChildIsInlineMenuButton && !(await this.isInlineMenuListVisible()))
) {
return;
}
if (
(lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) ||
(lastChildIsInlineMenuButton && this.isListVisible)
(lastChildIsInlineMenuButton && (await this.isInlineMenuListVisible()))
) {
globalThis.document.body.insertBefore(this.buttonElement, this.listElement);
return;