1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 20:24:01 +00:00

[PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays

This commit is contained in:
Cesar Gonzalez
2024-06-20 12:16:52 -05:00
parent 23c40d4a34
commit ff5c02dbdd
4 changed files with 284 additions and 292 deletions

View File

@@ -338,19 +338,49 @@ describe("OverlayBackground", () => {
const tabId = 1;
const topFrameId = 0;
const middleFrameId = 10;
const middleAdjacentFrameId = 11;
const bottomFrameId = 20;
let tab: chrome.tabs.Tab;
let sender: MockProxy<chrome.runtime.MessageSender>;
async function flushOverlayRepositionPromises() {
await flushPromises();
jest.advanceTimersByTime(1000);
await flushPromises();
}
beforeEach(() => {
jest.useFakeTimers();
tab = createChromeTabMock({ id: tabId });
sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
overlayBackground["focusedFieldData"] = mock<FocusedFieldData>({
tabId,
frameId: bottomFrameId,
});
subFrameOffsetsSpy[tabId] = new Map([
[topFrameId, { left: 1, top: 1, url: "https://top-frame.com" }],
[middleFrameId, { left: 2, top: 2, url: "https://middle-frame.com" }],
[bottomFrameId, { left: 3, top: 3, url: "https://bottom-frame.com" }],
[topFrameId, { left: 1, top: 1, url: "https://top-frame.com", parentFrameIds: [] }],
[
middleFrameId,
{ left: 2, top: 2, url: "https://middle-frame.com", parentFrameIds: [topFrameId] },
],
[
middleAdjacentFrameId,
{
left: 3,
top: 3,
url: "https://middle-adjacent-frame.com",
parentFrameIds: [topFrameId],
},
],
[
bottomFrameId,
{
left: 4,
top: 4,
url: "https://bottom-frame.com",
parentFrameIds: [topFrameId, middleFrameId],
},
],
]);
tabsSendMessageSpy.mockResolvedValue(
mock<SubFrameOffsetData>({
@@ -361,168 +391,214 @@ describe("OverlayBackground", () => {
);
});
describe("repositionInlineMenuForSubFrame", () => {
it("skips rebuilding sub frame offsets if the sender contains the currently focused field", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: bottomFrameId });
describe("triggerAutofillOverlayReposition", () => {
describe("checkShouldRepositionInlineMenu", () => {
let focusedFieldData: FocusedFieldData;
let repositionInlineMenuSpy: jest.SpyInstance;
sendMockExtensionMessage({ command: "repositionInlineMenuForSubFrame" }, sender);
expect(getFrameDetailsSpy).not.toHaveBeenCalled();
});
it("skips rebuilding sub frame offsets if the tab does not contain sub frames", () => {
const sender = mock<chrome.runtime.MessageSender>({
tab: createChromeTabMock({ id: 15 }),
frameId: 0,
beforeEach(() => {
focusedFieldData = createFocusedFieldDataMock({ tabId });
repositionInlineMenuSpy = jest.spyOn(overlayBackground as any, "repositionInlineMenu");
});
sendMockExtensionMessage({ command: "repositionInlineMenuForSubFrame" }, sender);
describe("blocking a reposition of the overlay", () => {
it("blocks repositioning when the focused field data is not set", async () => {
overlayBackground["focusedFieldData"] = undefined;
expect(getFrameDetailsSpy).not.toHaveBeenCalled();
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
await flushOverlayRepositionPromises();
expect(repositionInlineMenuSpy).not.toHaveBeenCalled();
});
it("blocks repositioning when the sender is from a different tab than the focused field", async () => {
const otherSender = mock<chrome.runtime.MessageSender>({ frameId: 1, tab: { id: 2 } });
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
otherSender,
);
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
await flushOverlayRepositionPromises();
expect(repositionInlineMenuSpy).not.toHaveBeenCalled();
});
it("blocks repositioning when the sender frame is for the focused field, but the inline menu is not visible", async () => {
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
sender,
);
tabsSendMessageSpy.mockImplementationOnce((_tab, message) => {
if (message.command === "checkIsAutofillInlineMenuButtonVisible") {
return Promise.resolve(false);
}
});
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
await flushOverlayRepositionPromises();
expect(repositionInlineMenuSpy).not.toHaveBeenCalled();
});
it("blocks repositioning when the sender frame is not a parent frame of the focused field", async () => {
focusedFieldData = createFocusedFieldDataMock({ tabId });
const otherFrameSender = mock<chrome.runtime.MessageSender>({
tab,
frameId: middleAdjacentFrameId,
});
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
otherFrameSender,
);
sender.frameId = bottomFrameId;
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
await flushOverlayRepositionPromises();
expect(repositionInlineMenuSpy).not.toHaveBeenCalled();
});
});
describe("allowing a reposition of the overlay", () => {
it("allows repositioning when the sender frame is for the focused field and the inline menu is visible, ", async () => {
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
sender,
);
tabsSendMessageSpy.mockImplementation((_tab, message) => {
if (message.command === "checkIsAutofillInlineMenuButtonVisible") {
return Promise.resolve(true);
}
});
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
await flushOverlayRepositionPromises();
expect(repositionInlineMenuSpy).toHaveBeenCalled();
});
});
});
it("rebuilds the sub frame offsets for a given tab", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
sendMockExtensionMessage({ command: "repositionAutofillInlineMenuForSubFrame" }, sender);
await flushPromises();
expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: topFrameId });
expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: bottomFrameId });
expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: middleFrameId });
});
// it("triggers an update of the inline menu position after rebuilding sub frames", async () => {
// jest.useFakeTimers();
// overlayBackground["delayedUpdateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650);
// const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
// jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent");
// it("rebuilds the sub frame offsets for a given tab", async () => {
// sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
// await flushOverlayRepositionPromises();
//
// sendMockExtensionMessage(
// {
// command: "repositionAutofillInlineMenuForSubFrame",
// triggerInlineMenuPositionUpdate: true,
// },
// sender,
// );
// await flushPromises();
// jest.advanceTimersByTime(650);
//
// expect(
// overlayBackground["updateInlineMenuPositionAfterRepositionEvent"],
// ).toHaveBeenCalled();
// expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: topFrameId });
// expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: bottomFrameId });
// expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: middleFrameId });
// });
});
describe("updateInlineMenuPositionAfterRepositionEvent", () => {
let sender: chrome.runtime.MessageSender;
async function flushInlineMenuUpdatePromises() {
await flushPromises();
jest.advanceTimersByTime(650);
await flushPromises();
}
beforeEach(() => {
sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
jest.useFakeTimers();
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: true,
});
});
it("skips updating the position of either inline menu element if a field is not currently focused", async () => {
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: false,
});
sendMockExtensionMessage({ command: "repositionInlineMenuForSubFrame" }, sender);
await flushInlineMenuUpdatePromises();
expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(
sender.tab,
{
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.Button,
},
{ frameId: 0 },
);
expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(
sender.tab,
{
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.List,
},
{ frameId: 0 },
);
});
it("updates the position of the inline menu elements", async () => {
sendMockExtensionMessage(
{
command: "repositionAutofillInlineMenuForSubFrame",
triggerInlineMenuPositionUpdate: true,
},
sender,
);
await flushInlineMenuUpdatePromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.Button,
},
{ frameId: 0 },
);
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.List,
},
{ frameId: 0 },
);
});
it("skips updating the inline menu list if the focused field has a value and the user status is not unlocked", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsSendMessageSpy.mockImplementation((_tab, message, _options) => {
if (message.command === "checkMostRecentlyFocusedFieldHasValue") {
return Promise.resolve(true);
}
return Promise.resolve();
});
sendMockExtensionMessage(
{
command: "repositionAutofillInlineMenuForSubFrame",
triggerInlineMenuPositionUpdate: true,
},
sender,
);
await flushInlineMenuUpdatePromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.Button,
},
{ frameId: 0 },
);
expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(
sender.tab,
{
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.List,
},
{ frameId: 0 },
);
});
});
// describe("updateInlineMenuPositionAfterRepositionEvent", () => {
// let sender: chrome.runtime.MessageSender;
//
// async function flushInlineMenuUpdatePromises() {
// await flushPromises();
// jest.advanceTimersByTime(650);
// await flushPromises();
// }
//
// beforeEach(() => {
// sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
// jest.useFakeTimers();
// sendMockExtensionMessage({
// command: "updateIsFieldCurrentlyFocused",
// isFieldCurrentlyFocused: true,
// });
// });
//
// it("skips updating the position of either inline menu element if a field is not currently focused", async () => {
// sendMockExtensionMessage({
// command: "updateIsFieldCurrentlyFocused",
// isFieldCurrentlyFocused: false,
// });
//
// sendMockExtensionMessage({ command: "repositionInlineMenuForSubFrame" }, sender);
// await flushInlineMenuUpdatePromises();
//
// expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(
// sender.tab,
// {
// command: "appendAutofillInlineMenuToDom",
// overlayElement: AutofillOverlayElement.Button,
// },
// { frameId: 0 },
// );
// expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(
// sender.tab,
// {
// command: "appendAutofillInlineMenuToDom",
// overlayElement: AutofillOverlayElement.List,
// },
// { frameId: 0 },
// );
// });
//
// it("updates the position of the inline menu elements", async () => {
// sendMockExtensionMessage(
// {
// command: "triggerAutofillOverlayReposition",
// triggerInlineMenuPositionUpdate: true,
// },
// sender,
// );
// await flushInlineMenuUpdatePromises();
//
// expect(tabsSendMessageSpy).toHaveBeenCalledWith(
// sender.tab,
// {
// command: "appendAutofillInlineMenuToDom",
// overlayElement: AutofillOverlayElement.Button,
// },
// { frameId: 0 },
// );
// expect(tabsSendMessageSpy).toHaveBeenCalledWith(
// sender.tab,
// {
// command: "appendAutofillInlineMenuToDom",
// overlayElement: AutofillOverlayElement.List,
// },
// { frameId: 0 },
// );
// });
//
// it("skips updating the inline menu list if the focused field has a value and the user status is not unlocked", async () => {
// activeAccountStatusMock$.next(AuthenticationStatus.Locked);
// tabsSendMessageSpy.mockImplementation((_tab, message, _options) => {
// if (message.command === "checkMostRecentlyFocusedFieldHasValue") {
// return Promise.resolve(true);
// }
// return Promise.resolve();
// });
//
// sendMockExtensionMessage(
// {
// command: "triggerAutofillOverlayReposition",
// triggerInlineMenuPositionUpdate: true,
// },
// sender,
// );
// await flushInlineMenuUpdatePromises();
//
// expect(tabsSendMessageSpy).toHaveBeenCalledWith(
// sender.tab,
// {
// command: "appendAutofillInlineMenuToDom",
// overlayElement: AutofillOverlayElement.Button,
// },
// { frameId: 0 },
// );
// expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(
// sender.tab,
// {
// command: "appendAutofillInlineMenuToDom",
// overlayElement: AutofillOverlayElement.List,
// },
// { frameId: 0 },
// );
// });
// });
});
describe("updating the overlay ciphers", () => {
@@ -1246,95 +1322,6 @@ 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,
});
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 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 });
@@ -1346,31 +1333,6 @@ describe("OverlayBackground", () => {
});
});
describe("rebuildSubFrameOffsets", () => {
it("triggers a rebuild of the sub frame offsets of the sender", async () => {
const buildSubFrameOffsetsSpy = jest.spyOn(
overlayBackground as any,
"buildSubFrameOffsets",
);
const tab = mock<chrome.tabs.Tab>({ id: 1 });
const frameId = 10;
subFrameOffsetsSpy[tab.id] = new Map([
[frameId, { left: 1, top: 1, url: "https://top-frame.com" }],
]);
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId });
sendMockExtensionMessage({ command: "rebuildSubFrameOffsets" }, sender);
await flushPromises();
expect(buildSubFrameOffsetsSpy).toHaveBeenCalledWith(
sender.tab,
frameId,
sender.url,
sender,
);
});
});
describe("destroyAutofillInlineMenuListeners", () => {
it("sends a message to the passed frameId that triggers a destruction of the inline menu listeners on that frame", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 0 });

View File

@@ -159,13 +159,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private initOverlayObservables() {
this.repositionInlineMenuSubject
.pipe(
debounceTime(500),
debounceTime(1000),
switchMap((sender) => this.repositionInlineMenu(sender)),
)
.subscribe();
this.rebuildSubFrameOffsetsSubject
.pipe(
throttleTime(650),
throttleTime(100),
switchMap((sender) => this.rebuildSubFrameOffsets(sender)),
)
.subscribe();
@@ -433,6 +433,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param sender - The sender of the message
*/
private async rebuildSubFrameOffsets(sender: chrome.runtime.MessageSender) {
this.cancelUpdateInlineMenuPositionSubject.next();
this.clearDelayedInlineMenuClosure();
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
@@ -460,7 +461,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
if (!(await this.checkIsInlineMenuButtonVisible(sender))) {
await this.toggleInlineMenuHidden(
void this.toggleInlineMenuHidden(
{ isInlineMenuHidden: false, setTransparentInlineMenu: true },
sender,
);
@@ -1119,30 +1120,25 @@ export class OverlayBackground implements OverlayBackgroundInterface {
*
* @param sender - The sender of the message
*/
private async checkShouldRepositionInlineMenu(
sender: chrome.runtime.MessageSender,
): Promise<boolean> {
private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean {
if (!this.focusedFieldData || sender.tab.id !== this.focusedFieldData.tabId) {
return false;
}
if (this.focusedFieldData.frameId === sender.frameId) {
return await this.checkIsInlineMenuButtonVisible(sender);
return true;
}
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
if (!subFrameOffsetsForTab) {
return false;
if (subFrameOffsetsForTab) {
for (const value of subFrameOffsetsForTab.values()) {
if (value?.parentFrameIds.includes(sender.frameId)) {
return true;
}
}
}
const parentFrameIds = new Set();
subFrameOffsetsForTab.forEach((subFrameOffsetData) =>
subFrameOffsetData?.parentFrameIds.forEach((parentFrameId) =>
parentFrameIds.add(parentFrameId),
),
);
return parentFrameIds.has(sender.frameId);
return false;
}
/**
@@ -1348,18 +1344,20 @@ export class OverlayBackground implements OverlayBackgroundInterface {
};
private async triggerOverlayReposition(sender: chrome.runtime.MessageSender) {
if (await this.checkShouldRepositionInlineMenu(sender)) {
await this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender);
if (this.checkShouldRepositionInlineMenu(sender)) {
this.cancelUpdateInlineMenuPositionSubject.next();
void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender);
this.repositionInlineMenuSubject.next(sender);
}
}
private async triggerSubFrameFocusInRebuild(sender: chrome.runtime.MessageSender) {
await this.rebuildSubFrameOffsets(sender);
this.rebuildSubFrameOffsetsSubject.next(sender);
this.repositionInlineMenuSubject.next(sender);
}
private repositionInlineMenu = async (sender: chrome.runtime.MessageSender) => {
this.cancelUpdateInlineMenuPositionSubject.next();
if (!this.isFieldCurrentlyFocused) {
await this.closeInlineMenuAfterReposition(sender);
return;
@@ -1375,10 +1373,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
if (this.focusedFieldData.frameId > 0 && sender.frameId !== this.focusedFieldData.frameId) {
if (this.focusedFieldData.frameId > 0) {
this.rebuildSubFrameOffsetsSubject.next(sender);
}
this.cancelUpdateInlineMenuPositionSubject.next();
this.startUpdateInlineMenuPositionSubject.next(sender);
};

View File

@@ -1000,7 +1000,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
globalThis.addEventListener(
EVENTS.SCROLL,
this.useEventHandlersMemo(
throttle(this.handleOverlayRepositionEvent, 200),
throttle(this.handleOverlayRepositionEvent, 150),
AUTOFILL_OVERLAY_ON_SCROLL,
),
{
@@ -1010,7 +1010,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
globalThis.addEventListener(
EVENTS.RESIZE,
this.useEventHandlersMemo(
throttle(this.handleOverlayRepositionEvent, 200),
throttle(this.handleOverlayRepositionEvent, 150),
AUTOFILL_OVERLAY_ON_RESIZE,
),
);

View File

@@ -331,6 +331,12 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri
return element.getAttribute(attributeName);
}
/**
* Throttles a callback function to run at most once every `limit` milliseconds.
*
* @param callback - The callback function to throttle.
* @param limit - The time in milliseconds to throttle the callback.
*/
export function throttle(callback: () => void, limit: number) {
let waitingDelay = false;
return function (...args: unknown[]) {
@@ -341,3 +347,28 @@ export function throttle(callback: () => void, limit: number) {
}
};
}
/**
* Debounces a callback function to run after a certain amount of time has passed.
*
* @param callback - The callback function to debounce.
* @param wait - The time in milliseconds to wait before running the callback.
* @param immediate - Determines whether the callback should run immediately.
*/
export function debounce(callback: () => void, wait: number, immediate?: boolean) {
let timeoutId: NodeJS.Timeout | number | null = null;
return (...args: unknown[]) => {
if (immediate && !timeoutId) {
callback.apply(this, args);
}
if (timeoutId) {
globalThis.clearTimeout(timeoutId);
}
timeoutId = globalThis.setTimeout(() => {
callback.apply(this, args);
timeoutId = null;
}, wait);
};
}