fix(pwa): focus the open window on a notification tap instead of reloading it

Tapping a reminder called `navigate()` on the first app window even
when it already showed the target page: a full document load that
dropped the query cache and any open sheet. The rejection of an
unawaited `navigate()` on an uncontrolled window went unhandled.

A window already on the target is now only focused, and preferred over
the first window. Otherwise the window is focused, then navigated; if
that fails a new window opens.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 20:39:59 +01:00
1 parent 25d087aaa2
commit 61f9447887
2 files changed
+113 -7

No files matched your search

+15 -7
View File
@@ -580,17 +580,25 @@ self.addEventListener("notificationclick", (event) => {
safeUrl = "/";
}
const target = new URL(safeUrl, self.location.origin).href;
event.waitUntil(
self.clients
.matchAll({ type: "window", includeUncontrolled: true })
.then((clientList) => {
for (const client of clientList) {
if (client.url.includes(self.location.origin) && "focus" in client) {
client.navigate(safeUrl);
return client.focus();
}
}
return self.clients.openWindow(safeUrl);
const windows = clientList.filter(
(c) => c.url.startsWith(self.location.origin + "/") && "focus" in c,
);
// A window already on the target only needs focus: navigating it
// would reload the app and drop whatever is open there.
const there = windows.find((c) => c.url === target);
if (there) return there.focus();
const client = windows[0];
if (!client) return self.clients.openWindow(safeUrl);
// Focus first, while the tap still counts as user activation.
return client
.focus()
.then(() => client.navigate(safeUrl))
.catch(() => self.clients.openWindow(safeUrl));
}),
);
});
+98
View File
@@ -1110,3 +1110,101 @@ describe("sw.js — push handler", () => {
expect(badge.value).toBe(3);
});
});
// ── notification tap ─────────────────────────────────────────────────────────
interface FakeWindow {
url: string;
focused: number;
navigatedTo: string[];
focus: () => Promise<FakeWindow>;
navigate: (url: string) => Promise<FakeWindow>;
}
function fakeWindow(path: string, navigateFails = false): FakeWindow {
const win: FakeWindow = {
url: `${ORIGIN}${path}`,
focused: 0,
navigatedTo: [],
focus: async () => {
win.focused += 1;
return win;
},
navigate: async (url: string) => {
if (navigateFails) throw new TypeError("not controlled");
win.navigatedTo.push(url);
return win;
},
};
return win;
}
async function tapNotification(
harness: SwHarness,
windows: FakeWindow[],
url: string,
): Promise<string[]> {
const opened: string[] = [];
(harness.context.self as { clients: unknown }).clients = {
claim: async () => {},
matchAll: async () => windows,
openWindow: async (target: string) => {
opened.push(target);
return null;
},
};
let settled: Promise<unknown> = Promise.resolve();
harness.listeners.get("notificationclick")!({
notification: { close: () => {}, data: { url } },
waitUntil: (p: Promise<unknown>) => {
settled = p;
},
});
await settled;
return opened;
}
describe("sw.js — notification tap", () => {
it("only focuses a window that is already on the target", async () => {
const harness = bootServiceWorker();
const win = fakeWindow("/medications");
const opened = await tapNotification(harness, [win], "/medications");
expect(win.focused).toBe(1);
expect(win.navigatedTo).toEqual([]);
expect(opened).toEqual([]);
});
it("prefers the window on the target over the first one", async () => {
const harness = bootServiceWorker();
const other = fakeWindow("/");
const there = fakeWindow("/mood?add=1");
await tapNotification(harness, [other, there], "/mood?add=1");
expect(there.focused).toBe(1);
expect(other.focused).toBe(0);
expect(other.navigatedTo).toEqual([]);
});
it("focuses and navigates a window that is elsewhere", async () => {
const harness = bootServiceWorker();
const win = fakeWindow("/");
const opened = await tapNotification(harness, [win], "/medications");
expect(win.focused).toBe(1);
expect(win.navigatedTo).toEqual(["/medications"]);
expect(opened).toEqual([]);
});
it("opens a window when none is open or navigating fails", async () => {
const harness = bootServiceWorker();
expect(await tapNotification(harness, [], "/medications")).toEqual([
"/medications",
]);
expect(
await tapNotification(harness, [fakeWindow("/", true)], "/medications"),
).toEqual(["/medications"]);
});
});