mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-11-10 14:44:04 +08:00
Web app: notify for a message only a poll stored, and test replays end to end
A message the poll stored before its live delivery has no unread marker yet, so the WebSocket delivery that follows marks it unread and notifies again, while a replayed message that came in live keeps its state. The e2e tests publish within one second and reload, so the reconnect and the startup poll replay the earlier messages: deleted ones stay gone, and read ones stay read without a new browser notification.
This commit is contained in:
1 parent
c43c475be3
commit
e6a6049d23
3 files changed
+85
-1
No files matched your search
@@ -9,6 +9,31 @@ let topic;
|
||||
// Browser notifications the app showed, recorded by the init script in beforeEach
|
||||
const shown = (page) => page.evaluate(() => window.shownNotifications.map((n) => n.body));
|
||||
|
||||
// Publishes messages within one second, right after a second starts. A since=<id> marker replays
|
||||
// every message from its second, so the earlier ones come back to a client that has the last one.
|
||||
const publishInOneSecond = async (request, topic, messages) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 - (Date.now() % 1000)));
|
||||
const times = [];
|
||||
for (const message of messages) {
|
||||
times.push((await (await request.post(`/${topic}`, { data: message })).json()).time);
|
||||
}
|
||||
expect(new Set(times).size, `published in one second: ${times}`).toBe(1);
|
||||
};
|
||||
|
||||
// Messages the topic's WebSocket received from the server, from now on
|
||||
const liveMessages = (page, topic) => {
|
||||
const received = [];
|
||||
page.on("websocket", (ws) => {
|
||||
if (new URL(ws.url()).pathname.endsWith(`/${topic}/ws`)) {
|
||||
ws.on("framereceived", (frame) => {
|
||||
const event = JSON.parse(frame.payload);
|
||||
if (event.event === "message") received.push(event.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
return received;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Record browser notifications instead of showing them; headless Chrome can't show them anyway
|
||||
await page.addInitScript(() => {
|
||||
@@ -268,6 +293,49 @@ test.describe("sequences", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("replays", () => {
|
||||
test("deleted messages don't come back when a reconnect replays them", async ({ page, request }) => {
|
||||
await publishInOneSecond(request, topic, ["first", "second", "third"]);
|
||||
const items = page.getByRole("listitem", { name: "Notification" });
|
||||
await expect(items).toHaveCount(3);
|
||||
for (const message of ["first", "second", "third"]) {
|
||||
await items.filter({ hasText: message }).getByRole("button", { name: "Delete" }).click();
|
||||
}
|
||||
await expect(items).toHaveCount(0);
|
||||
|
||||
// Reconnecting and the startup poll ask for since=<last>, and get the rest of its second again
|
||||
const live = liveMessages(page, topic);
|
||||
const polled = page.waitForResponse((r) => new URL(r.url()).pathname.endsWith(`/${topic}/json`));
|
||||
await page.reload();
|
||||
await page.clock.runFor(2000);
|
||||
expect(await (await polled).text()).toContain("first");
|
||||
await expect.poll(() => live).toContain("first");
|
||||
await page.waitForTimeout(1000); // Let the app store what it got
|
||||
await expect(items).toHaveCount(0);
|
||||
await expect(page.getByText("You haven't received any notifications for this topic yet.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("a message replayed on reconnect stays read and doesn't notify again", async ({ page, request }) => {
|
||||
await publishInOneSecond(request, topic, ["first", "second"]);
|
||||
await expect(page.getByRole("listitem", { name: "Notification" })).toHaveCount(2);
|
||||
|
||||
// Read them by opening the topic, then reconnect from another one, so one that comes back unread is counted
|
||||
const other = unique("other");
|
||||
await subscribe(page, other);
|
||||
await expect(page).toHaveTitle("(2) ntfy");
|
||||
await page.getByRole("button", { name: topic, exact: true }).click();
|
||||
await expect(page).toHaveTitle("ntfy");
|
||||
await page.getByRole("button", { name: other, exact: true }).click();
|
||||
const live = liveMessages(page, topic);
|
||||
await page.reload();
|
||||
await page.clock.runFor(2000);
|
||||
await expect.poll(() => live).toContain("first");
|
||||
await page.waitForTimeout(1000); // Let the app store what it got
|
||||
await expect(page).toHaveTitle("ntfy");
|
||||
expect(await shown(page)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("browser notifications", () => {
|
||||
test("a new message is shown", async ({ page, request }) => {
|
||||
await request.post(`/${topic}`, { data: "pop up" });
|
||||
|
||||
@@ -201,7 +201,13 @@ export class SubscriptionManager {
|
||||
// Add notification to database; check and add in one transaction, so a poll storing the
|
||||
// same message concurrently (see addNotifications) can't slip in between
|
||||
const added = await this.db.transaction("rw", this.db.notifications, async () => {
|
||||
if (await this.db.notifications.get(notification.id)) {
|
||||
// A replayed message keeps its local state (e.g. read or deleted), but one that only a poll
|
||||
// stored (no "new" marker, see addNotifications) wasn't delivered live yet, so it's new
|
||||
const existing = await this.db.notifications.get(notification.id);
|
||||
if (existing && existing.new === undefined) {
|
||||
await this.db.notifications.update(notification.id, { new: 1 });
|
||||
return true;
|
||||
} else if (existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,16 @@ describe("SubscriptionManager.addNotification", () => {
|
||||
expect((await db.subscriptions.get(subscriptionId)).last).toBe("m2");
|
||||
});
|
||||
|
||||
it("marks a message the poll stored as unread when the WebSocket delivers it", async () => {
|
||||
const { db, manager } = await setup();
|
||||
await manager.addNotifications(subscriptionId, [message("m1")]);
|
||||
|
||||
// The poll stored it before the live delivery arrived; the user hasn't seen it yet
|
||||
expect(await manager.addNotification(subscriptionId, message("m1"))).toBe(true);
|
||||
|
||||
expect((await db.notifications.get("m1")).new).toBe(1);
|
||||
});
|
||||
|
||||
it("does not store delete and clear events", async () => {
|
||||
const { db, manager } = await setup();
|
||||
expect(await manager.addNotification(subscriptionId, message("d1", { event: "message_delete", sequence_id: "s1" }))).toBe(false);
|
||||
|
||||
Reference in new issue
Block a user