1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-19 01:51:27 +01:00

[PM-5189] Fixing a weird side issue that appears when a frame within the page triggers a reposition after the inline menu has been built

This commit is contained in:
Cesar Gonzalez 2024-06-06 13:29:48 -05:00
parent e2c9fa4b59
commit b802f3e7ed
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
4 changed files with 154 additions and 16 deletions

View File

@ -228,7 +228,7 @@ describe("OverlayBackground", () => {
await flushPromises();
expect(subFrameOffsetsSpy[tabId]).toStrictEqual(
new Map([[1, { left: 4, top: 4, url: "url" }]]),
new Map([[1, { left: 4, top: 4, url: "url", parentFrameIds: [1, 0] }]]),
);
expect(pageDetailsForTabSpy[tabId].size).toBe(2);
});
@ -255,7 +255,7 @@ describe("OverlayBackground", () => {
expect(getFrameDetailsSpy).toHaveBeenCalledTimes(1);
expect(subFrameOffsetsSpy[tabId]).toStrictEqual(
new Map([[1, { left: 0, top: 0, url: "url" }]]),
new Map([[1, { left: 0, top: 0, url: "url", parentFrameIds: [] }]]),
);
});
@ -1194,7 +1194,7 @@ describe("OverlayBackground", () => {
});
});
describe("checkIsAutofillInlineMenuListVisible", () => {
describe("checkIsAutofillInlineMenuListVisible message handler", () => {
it("sends a message to the top frame of the tab to identify if the inline menu list is visible", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
@ -1208,6 +1208,131 @@ describe("OverlayBackground", () => {
});
});
describe("checkShouldRepositionInlineMenu message handler", () => {
const tabId = 1;
const frameId = 1;
const sender = mock<chrome.runtime.MessageSender>({
tab: createChromeTabMock({ id: tabId }),
frameId,
});
const otherSender = mock<chrome.runtime.MessageSender>({
tab: createChromeTabMock({ id: tabId }),
frameId: 2,
});
beforeEach(() => {
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: true,
});
});
it("returns false if the focused field data is not set", async () => {
sendMockExtensionMessage(
{ command: "checkShouldRepositionInlineMenu" },
sender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(false);
});
it("returns false if the sender is from a different tab than the focused field", async () => {
const focusedFieldData = createFocusedFieldDataMock();
const otherSender = mock<chrome.runtime.MessageSender>({ frameId: 1, tab: { id: 2 } });
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
otherSender,
);
sendMockExtensionMessage(
{ command: "checkShouldRepositionInlineMenu" },
sender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(false);
});
it("returns false if the field is not currently focused", async () => {
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: false,
});
const focusedFieldData = createFocusedFieldDataMock();
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
sendMockExtensionMessage(
{ command: "checkShouldRepositionInlineMenu" },
sender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(false);
});
it("returns true if the focused field's frame id is equal to the sender's frame id", async () => {
const focusedFieldData = createFocusedFieldDataMock();
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
sendMockExtensionMessage(
{ command: "checkShouldRepositionInlineMenu" },
sender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(true);
});
describe("when the focused field is in a different frame than the sender", () => {
it("returns false if the tab does not contain and sub frame offset data", async () => {
const focusedFieldData = createFocusedFieldDataMock({ frameId: 2 });
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
sendMockExtensionMessage(
{ command: "checkShouldRepositionInlineMenu" },
otherSender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(false);
});
it("returns true if the sender's frameId is present in any of the parentFrameIds of the tab's sub frames", async () => {
const focusedFieldData = createFocusedFieldDataMock();
subFrameOffsetsSpy[tabId] = new Map([
[frameId, { left: 1, top: 1, url: "https://top-frame.com", parentFrameIds: [2, 0] }],
]);
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
sendMockExtensionMessage(
{ command: "checkShouldRepositionInlineMenu" },
otherSender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(true);
});
});
});
describe("getCurrentTabFrameId message handler", () => {
it("returns the sender's frame id", async () => {
const sender = mock<chrome.runtime.MessageSender>({ frameId: 1 });
sendMockExtensionMessage({ command: "getCurrentTabFrameId" }, sender, sendResponse);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(1);
});
});
describe("unlockCompleted", () => {
let updateOverlayCiphersSpy: jest.SpyInstance;

View File

@ -89,7 +89,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
checkIsAutofillInlineMenuListVisible: ({ sender }) =>
this.checkIsAutofillInlineMenuListVisible(sender),
checkShouldRepositionInlineMenu: ({ sender }) => this.checkShouldRepositionInlineMenu(sender),
getCurrentTabFrameId: ({ sender }) => this.getCurrentFrameId(sender),
getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender),
updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender),
rebuildSubFrameOffsets: ({ sender }) => this.rebuildSubFrameOffsets(sender),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
@ -258,7 +258,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
pageDetailsMap.set(sender.frameId, pageDetails);
}
private getCurrentFrameId(sender: chrome.runtime.MessageSender) {
/**
* Returns the frameId, called when calculating sub frame offsets within the tab.
* Is used to determine if we should reposition the inline menu when a resize event
* occurs within a frame.
*
* @param sender - The sender of the message
*/
private getSenderFrameId(sender: chrome.runtime.MessageSender) {
return sender.frameId;
}
@ -1012,6 +1019,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
);
}
/**
* Handles verifying whether the inline menu should be repositioned. This is used to
* guard against removing the inline menu when other frames trigger a resize event.
*
* @param sender - The sender of the message
*/
private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean {
if (
!this.focusedFieldData ||

View File

@ -1078,8 +1078,7 @@ describe("AutofillOverlayContentService", () => {
});
describe("handleOverlayRepositionEvent", () => {
let isInlineMenuButtonVisibleSpy: jest.SpyInstance;
let isInlineMenuListVisibleSpy: jest.SpyInstance;
let checkShouldRepositionInlineMenuSpy: jest.SpyInstance;
beforeEach(() => {
document.body.innerHTML = `
@ -1093,11 +1092,8 @@ describe("AutofillOverlayContentService", () => {
) as ElementWithOpId<HTMLInputElement>;
autofillOverlayContentService["mostRecentlyFocusedField"] = usernameField;
autofillOverlayContentService["setOverlayRepositionEventListeners"]();
isInlineMenuButtonVisibleSpy = jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuButtonVisible")
.mockResolvedValue(true);
isInlineMenuListVisibleSpy = jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
checkShouldRepositionInlineMenuSpy = jest
.spyOn(autofillOverlayContentService as any, "checkShouldRepositionInlineMenu")
.mockResolvedValue(true);
jest
.spyOn(autofillOverlayContentService as any, "recentlyFocusedFieldIsCurrentlyFocused")
@ -1105,8 +1101,7 @@ describe("AutofillOverlayContentService", () => {
});
it("skips handling the overlay reposition event if the overlay button and list elements are not visible", async () => {
isInlineMenuButtonVisibleSpy.mockResolvedValue(false);
isInlineMenuListVisibleSpy.mockResolvedValue(false);
checkShouldRepositionInlineMenuSpy.mockResolvedValue(false);
globalThis.dispatchEvent(new Event(EVENTS.RESIZE));
await flushPromises();
@ -1124,12 +1119,13 @@ describe("AutofillOverlayContentService", () => {
});
});
it("clears the user interaction timeout", () => {
it("clears the user interaction timeout", async () => {
jest.useFakeTimers();
const clearTimeoutSpy = jest.spyOn(globalThis, "clearTimeout");
autofillOverlayContentService["userInteractionEventTimeout"] = setTimeout(jest.fn(), 123);
globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
await flushPromises();
expect(clearTimeoutSpy).toHaveBeenCalledWith(expect.anything());
});

View File

@ -772,7 +772,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* repositioning of existing overlay elements.
*/
private handleOverlayRepositionEvent = async () => {
if (!(await this.sendExtensionMessage("checkShouldRepositionInlineMenu"))) {
if (!(await this.checkShouldRepositionInlineMenu())) {
return;
}
@ -1029,6 +1029,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
}
private async checkShouldRepositionInlineMenu() {
return (await this.sendExtensionMessage("checkShouldRepositionInlineMenu")) === true;
}
/**
* Destroys the autofill overlay content service. This method will
* disconnect the mutation observers and remove all event listeners.