fix(boards): open tabs before async fetch in bulk action

Pre-open blank tabs synchronously to preserve browser user-activation, then navigate those tab handles after app URLs are fetched. Also close surplus tabs and update tests for the new flow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
ajnartandCopilot committed 2026-07-06 10:05:57 +02:00
1 parent b97856dcb8
commit 5d9bfa3978
2 files changed
+27 -9

No files matched your search

@@ -19,13 +19,23 @@ export const openAppsInNewTabs = async (
if (appIds.length === 0) return;
const distinctAppIds = [...new Set(appIds)];
const openedWindows = distinctAppIds.map(() => window.open("", "_blank", "noopener,noreferrer"));
for (const openedWindow of openedWindows) {
if (!openedWindow) continue;
openedWindow.opener = null;
}
const apps = await fetchAppsByIds(distinctAppIds);
const appsWithUrls = apps.filter((app) => app.href && app.href.length > 0);
const tabsToClose = openedWindows.slice(appsWithUrls.length).filter((openedWindow): openedWindow is Window => Boolean(openedWindow));
for (const tabToClose of tabsToClose) {
tabToClose.close();
}
for (const app of appsWithUrls) {
const openedWindow = window.open(app.href ?? undefined, "_blank", "noopener,noreferrer");
for (const [index, app] of appsWithUrls.entries()) {
const openedWindow = openedWindows[index];
if (openedWindow) {
openedWindow.opener = null;
openedWindow.location.href = app.href;
continue;
}
@@ -3,8 +3,11 @@ import { describe, expect, test, vi } from "vitest";
import { openAppsInNewTabs } from "../open-apps-in-new-tabs";
describe("openAppsInNewTabs", () => {
test("opens fetched apps with URLs in new tabs with noopener", async () => {
const open = vi.spyOn(window, "open").mockReturnValue({ opener: null } as unknown as Window);
test("opens tabs synchronously and navigates them to fetched URLs", async () => {
const openedWindowOne = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const openedWindowTwo = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const openedWindowThree = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const open = vi.spyOn(window, "open").mockReturnValueOnce(openedWindowOne).mockReturnValueOnce(openedWindowTwo).mockReturnValueOnce(openedWindowThree);
const fetchAppsByIds = vi
.fn()
.mockResolvedValue([{ href: "https://example.com/one" }, { href: null }, { href: "https://example.com/two" }]);
@@ -17,14 +20,19 @@ describe("openAppsInNewTabs", () => {
});
expect(fetchAppsByIds).toHaveBeenCalledWith(["app-one", "app-two", "app-three"]);
expect(open).toHaveBeenCalledTimes(2);
expect(open).toHaveBeenNthCalledWith(1, "https://example.com/one", "_blank", "noopener,noreferrer");
expect(open).toHaveBeenNthCalledWith(2, "https://example.com/two", "_blank", "noopener,noreferrer");
expect(open).toHaveBeenCalledTimes(3);
expect(open).toHaveBeenNthCalledWith(1, "", "_blank", "noopener,noreferrer");
expect(open).toHaveBeenNthCalledWith(2, "", "_blank", "noopener,noreferrer");
expect(open).toHaveBeenNthCalledWith(3, "", "_blank", "noopener,noreferrer");
expect(openedWindowOne.location.href).toBe("https://example.com/one");
expect(openedWindowTwo.location.href).toBe("https://example.com/two");
expect(openedWindowThree.close).toHaveBeenCalledTimes(1);
expect(openConfirmModal).not.toHaveBeenCalled();
});
test("de-duplicates app IDs before fetching", async () => {
const open = vi.spyOn(window, "open").mockReturnValue({ opener: null } as unknown as Window);
const openedWindow = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const open = vi.spyOn(window, "open").mockReturnValue(openedWindow);
const fetchAppsByIds = vi.fn().mockResolvedValue([{ href: "https://example.com/one" }]);
const openConfirmModal = vi.fn();