diff --git a/artifacts/api-server/tests/executive-meetings-row-color.test.mjs b/artifacts/api-server/tests/executive-meetings-row-color.test.mjs index 485e597f..a0fdb793 100644 --- a/artifacts/api-server/tests/executive-meetings-row-color.test.mjs +++ b/artifacts/api-server/tests/executive-meetings-row-color.test.mjs @@ -19,6 +19,7 @@ import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import pg from "pg"; +import { io as ioClient } from "socket.io-client"; const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080"; const DATABASE_URL = process.env.DATABASE_URL; @@ -306,6 +307,68 @@ test("PATCH rowColor: non-mutate viewer role gets 403 and the row colour is unch ); }); +test("PATCH rowColor: the realtime executive_meetings_changed event fires for the affected date so other tabs / devices know to re-fetch", async () => { + // This is the contract that makes "shared" actually feel realtime — + // without it, a second viewer would only see the colour change after + // they manually refreshed. The frontend listens for this exact event + // (see use-notifications-socket.ts) and invalidates the day query. + const meeting = await createMeeting( + "RowColor socket", + "بث اللون عبر السوكيت", + ); + + const events = []; + const socket = ioClient(API_BASE, { + path: "/api/socket.io", + transports: ["websocket"], + forceNew: true, + reconnection: false, + extraHeaders: { Cookie: adminCookie }, + }); + await new Promise((resolve, reject) => { + socket.on("executive_meetings_changed", (payload) => { + events.push(payload); + }); + socket.on("connect", resolve); + socket.on("connect_error", reject); + }); + + try { + const res = await api( + adminCookie, + "PATCH", + `/api/executive-meetings/${meeting.id}`, + { rowColor: "gray" }, + ); + assert.equal(res.status, 200); + + // Give the broadcast a moment to land. 400ms is plenty for a + // localhost socket and well under the test timeout. + const start = Date.now(); + while (events.length === 0 && Date.now() - start < 400) { + await new Promise((r) => setTimeout(r, 25)); + } + assert.ok( + events.length > 0, + "executive_meetings_changed should fire after a rowColor PATCH", + ); + // Some payloads also include an array of dates; we just need the + // current day to be addressed in any of them. + const sawToday = events.some((p) => { + if (!p) return false; + if (typeof p.date === "string" && p.date === today) return true; + if (Array.isArray(p.dates) && p.dates.includes(today)) return true; + return false; + }); + assert.ok( + sawToday, + `event should reference today's date (${today}); got ${JSON.stringify(events)}`, + ); + } finally { + socket.disconnect(); + } +}); + test("PATCH rowColor: each successful change writes an audit row attributed to the acting user", async () => { const meeting = await createMeeting("RowColor audit", "سجل لون الصف");