mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
Fix Safari pinned tabs causing autofill to be wrong (#13755)
Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>
This commit is contained in:
@@ -589,4 +589,113 @@ describe("BrowserApi", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Safari sometimes returns >1 tabs unexpectedly even when
|
||||||
|
* specificing a `windowId` or `currentWindow: true` query option.
|
||||||
|
*
|
||||||
|
* For example, when there are >=2 windows with an active pinned tab,
|
||||||
|
* the pinned tab will always be included as the first entry in the array,
|
||||||
|
* while the correct tab is included as the second entry.
|
||||||
|
*
|
||||||
|
* These tests can remain as verification when Safari fixes this bug.
|
||||||
|
*/
|
||||||
|
describe.each([{ isSafariApi: true }, { isSafariApi: false }])(
|
||||||
|
"SafariTabsQuery %p",
|
||||||
|
({ isSafariApi }) => {
|
||||||
|
let originalIsSafariApi = BrowserApi.isSafariApi;
|
||||||
|
const expectedWindowId = 10;
|
||||||
|
const wrongWindowId = expectedWindowId + 1;
|
||||||
|
const raceConditionWindowId = expectedWindowId + 2;
|
||||||
|
const mismatchedWindowId = expectedWindowId + 3;
|
||||||
|
|
||||||
|
const resolvedTabsQueryResult = [
|
||||||
|
mock<chrome.tabs.Tab>({
|
||||||
|
title: "tab[0] is a pinned tab from another window",
|
||||||
|
pinned: true,
|
||||||
|
windowId: wrongWindowId,
|
||||||
|
}),
|
||||||
|
mock<chrome.tabs.Tab>({
|
||||||
|
title: "tab[1] is the tab with the correct foreground window",
|
||||||
|
windowId: expectedWindowId,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
function mockCurrentWindowId(id: number | null) {
|
||||||
|
jest
|
||||||
|
.spyOn(BrowserApi, "getCurrentWindow")
|
||||||
|
.mockResolvedValue(mock<chrome.windows.Window>({ id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalIsSafariApi = BrowserApi.isSafariApi;
|
||||||
|
BrowserApi.isSafariApi = isSafariApi;
|
||||||
|
mockCurrentWindowId(expectedWindowId);
|
||||||
|
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue(resolvedTabsQueryResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
BrowserApi.isSafariApi = originalIsSafariApi;
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([BrowserApi.getTabFromCurrentWindow, BrowserApi.getTabFromCurrentWindowId])(
|
||||||
|
"%p",
|
||||||
|
(getCurrTabFn) => {
|
||||||
|
it("returns the first tab when the query result has one tab", async () => {
|
||||||
|
const expectedSingleTab = resolvedTabsQueryResult[0];
|
||||||
|
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([expectedSingleTab]);
|
||||||
|
const actualTab = await getCurrTabFn();
|
||||||
|
expect(actualTab).toBe(expectedSingleTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the first tab when the current window ID is mismatched", async () => {
|
||||||
|
mockCurrentWindowId(mismatchedWindowId);
|
||||||
|
const actualTab = await getCurrTabFn();
|
||||||
|
expect(actualTab).toBe(resolvedTabsQueryResult[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the first tab when the current window ID is unavailable", async () => {
|
||||||
|
mockCurrentWindowId(null);
|
||||||
|
const actualTab = await getCurrTabFn();
|
||||||
|
expect(actualTab).toBe(resolvedTabsQueryResult[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isSafariApi) {
|
||||||
|
it("returns the tab with the current window ID", async () => {
|
||||||
|
const actualTab = await getCurrTabFn();
|
||||||
|
expect(actualTab.windowId).toBe(expectedWindowId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`returns the tab with the current window ID at the time of calling [Function ${getCurrTabFn.name}]`, async () => {
|
||||||
|
jest.spyOn(BrowserApi, "tabsQuery").mockImplementation(() => {
|
||||||
|
/*
|
||||||
|
* Simulate rapid clicking/switching between windows, e.g.
|
||||||
|
* 1. From Window A, call `getCurrTabFn()`
|
||||||
|
* 2. getCurrTabFn() calls `await BrowserApi.tabsQuery()`
|
||||||
|
* 3. Users switches to Window B before the `await` returns
|
||||||
|
* 4. getCurrTabFn() calls `await BrowserApi.getCurrentWindow()`
|
||||||
|
* ^ This now returns Window B and filters the results erroneously
|
||||||
|
*/
|
||||||
|
mockCurrentWindowId(raceConditionWindowId);
|
||||||
|
|
||||||
|
return Promise.resolve(resolvedTabsQueryResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualTab = await getCurrTabFn();
|
||||||
|
expect(actualTab.windowId).toBe(expectedWindowId);
|
||||||
|
});
|
||||||
|
} /* !isSafariApi */ else {
|
||||||
|
it("falls back to tabsQueryFirst", async () => {
|
||||||
|
const tabsQueryFirstSpy = jest.spyOn(BrowserApi, "tabsQueryFirst");
|
||||||
|
const actualTab = await getCurrTabFn();
|
||||||
|
|
||||||
|
expect(tabsQueryFirstSpy).toHaveBeenCalled();
|
||||||
|
expect(actualTab).toBe(resolvedTabsQueryResult[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export class BrowserApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
|
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
|
||||||
return await BrowserApi.tabsQueryFirst({
|
return await BrowserApi.tabsQueryFirstCurrentWindowForSafari({
|
||||||
active: true,
|
active: true,
|
||||||
windowId: chrome.windows.WINDOW_ID_CURRENT,
|
windowId: chrome.windows.WINDOW_ID_CURRENT,
|
||||||
});
|
});
|
||||||
@@ -153,7 +153,7 @@ export class BrowserApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
|
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
|
||||||
return await BrowserApi.tabsQueryFirst({
|
return await BrowserApi.tabsQueryFirstCurrentWindowForSafari({
|
||||||
active: true,
|
active: true,
|
||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
});
|
});
|
||||||
@@ -197,6 +197,51 @@ export class BrowserApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for {@link BrowserApi.tabsQueryFirst}.
|
||||||
|
*
|
||||||
|
* Safari sometimes returns >1 tabs unexpectedly even when
|
||||||
|
* specificing a `windowId` or `currentWindow: true` query option.
|
||||||
|
*
|
||||||
|
* For all of these calls,
|
||||||
|
* ```
|
||||||
|
* await chrome.tabs.query({active: true, currentWindow: true})
|
||||||
|
* await chrome.tabs.query({active: true, windowId: chrome.windows.WINDOW_ID_CURRENT})
|
||||||
|
* await chrome.tabs.query({active: true, windowId: 10})
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Safari could return:
|
||||||
|
* ```
|
||||||
|
* [
|
||||||
|
* {windowId: 2, pinned: true, title: "Incorrect tab in another window", …},
|
||||||
|
* {windowId: 10, title: "Correct tab in foreground", …},
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This function captures the current window ID manually before running the query,
|
||||||
|
* then finds and returns the tab with the matching window ID.
|
||||||
|
*
|
||||||
|
* See the `SafariTabsQuery` tests in `browser-api.spec.ts`.
|
||||||
|
*
|
||||||
|
* This workaround can be removed when Safari fixes this bug.
|
||||||
|
*/
|
||||||
|
static async tabsQueryFirstCurrentWindowForSafari(
|
||||||
|
options: chrome.tabs.QueryInfo,
|
||||||
|
): Promise<chrome.tabs.Tab> | null {
|
||||||
|
if (!BrowserApi.isSafariApi) {
|
||||||
|
return await BrowserApi.tabsQueryFirst(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWindowId = (await BrowserApi.getCurrentWindow()).id;
|
||||||
|
const tabs = await BrowserApi.tabsQuery(options);
|
||||||
|
|
||||||
|
if (tabs.length <= 1 || currentWindowId == null) {
|
||||||
|
return tabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs.find((t) => t.windowId === currentWindowId) ?? tabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
static tabSendMessageData(
|
static tabSendMessageData(
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
command: string,
|
command: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user