/** * Scenario: database-view-realtime-sse * * Verifies the full SSE end-to-end flow: * 1. Browser A has a page open with a database-view (grid). * 2. An external API call (via Baserow API + bridge webhook) modifies a row. * 3. Browser A sees the updated value without any manual reload. * * This test exercises the full chain: * Baserow webhook -> bridge handler -> Redis Streams -> SSE endpoint * -> useDatabaseRealtimeUpdates hook -> React Query invalidation -> re-render. * * SSE propagation timing: the bridge polls Redis every 100ms (default). We * allow up to 15 seconds for the update to appear — well above the 99th * percentile for local network. */ import { test, expect } from "@playwright/test"; import * as fs from "fs"; import * as path from "path"; import type { BaserowSeed } from "../fixtures/baserow"; import { updateRowViaBaserowApi } from "../fixtures/baserow"; const BASE_URL = process.env.E2E_DOCMOST_URL ?? "http://localhost:5173"; const SEED_FILE = path.resolve(__dirname, "../.auth/baserow-seed.json"); const SSE_TEST_VALUE = "SSE Updated Task"; test.describe("database-view realtime SSE", () => { let seed: BaserowSeed; test.beforeAll(() => { if (!fs.existsSync(SEED_FILE)) { throw new Error(`Seed file not found at ${SEED_FILE}.`); } seed = JSON.parse(fs.readFileSync(SEED_FILE, "utf-8")) as BaserowSeed; }); test( "row update via Baserow API propagates to open browser via SSE without reload", async ({ page, request }) => { // Open a page with a database-view node (depends on insert spec having run). await page.goto(BASE_URL); const tableRenderer = page .getByTestId("table-renderer") .or(page.locator("[data-node-type='database-view'] table")) .first(); const rendererVisible = await tableRenderer .isVisible({ timeout: 5_000 }) .catch(() => false); if (!rendererVisible) { test.skip( true, "No database-view page found. Run database-view-insert spec first.", ); return; } // Confirm the initial value is present. await expect(page.getByText("Task Beta")).toBeVisible({ timeout: 10_000 }); // Verify the SSE connection is established by checking the bridge events endpoint. // We do this by intercepting XHR/fetch — but SSE uses EventSource natively. // Instead we monitor the page console for the SSE open event log. // The bridge logs "SSE client connected" on open — this doesn't leak to the client. // We rely purely on the observable DOM change after the Baserow API call. // Wait a moment to ensure the SSE connection is established and stable. await page.waitForTimeout(2_000); // Modify row 2 ("Task Beta") via the Baserow API directly. // This bypasses the bridge write path — Baserow will emit a webhook to the bridge, // which publishes to Redis Streams, which the SSE connection picks up. const rowIdToUpdate = seed.rowIds[1]; // Task Beta await updateRowViaBaserowApi(request, seed.token, seed.tableId, rowIdToUpdate, { [seed.primaryFieldName]: SSE_TEST_VALUE, }); // The browser should receive the SSE event and invalidate the React Query cache, // causing the table to re-render with the updated value — WITHOUT a page reload. await expect(page.getByText(SSE_TEST_VALUE)).toBeVisible({ timeout: 15_000, }); // The old value "Task Beta" must be gone from the rendered table. await expect(page.getByRole("cell", { name: "Task Beta" })).toBeHidden({ timeout: 5_000, }); }, ); test.afterAll(async ({ request }) => { // Restore "Task Beta". if (!seed?.rowIds[1]) return; await updateRowViaBaserowApi(request, seed.token, seed.tableId, seed.rowIds[1], { [seed.primaryFieldName]: "Task Beta", }); }); });