mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
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:
1 parent
25d087aaa2
commit
61f9447887
2 files changed
+113
-7
No files matched your search
+15
-7
@@ -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));
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user