import { expect, test, type Page } from "@playwright/test"; import { dcUser } from "./helpers"; import { actor, api, closeAll } from "./shophelpers"; import { L, makeDraft, makeProject, makeUnit, pullWithHeat, runSuffix, type Made } from "./pmhelpers"; // Phase 8 presence on the CI stack, modelled on the old sync.js presence and the "Also here" line. clerk.clara and // wh.wade make one packet Ready to assign in 03. sup.wes and weld.andy open it in two browser contexts: each sees // "Also in this packet now:" with the other's name and role. weld.andy closes the page: the callout on sup.wes's // page clears within 10 s. weld.andy opens it again, then sup.wes locks the session through the API: weld.andy's // callout drops sup.wes. Every UI label used here is in L (pmhelpers.ts). let RUN = ""; let P: Made; function callout(page: Page) { return page.getByTestId(L.presence); } async function openPacket(page: Page, id: number): Promise { await page.goto(`/packets/${id}`); await expect(page.getByRole("heading").first()).toBeVisible(); } test.describe.serial("presence", () => { test.setTimeout(180_000); test("clerk.clara and wh.wade make the packet", async ({ browser }, info) => { RUN = runSuffix(info.project.name); const clara = await actor(browser, "clerk.clara"); const wade = await actor(browser, "wh.wade"); const proj = await makeProject(clara, `PR${RUN}`, { name: "Presence job" }); const unit = await makeUnit(clara, Number(proj.id), "001"); P = await makeDraft(clara, unit, `Presence ${RUN}`, { send: "warehouse" }); await pullWithHeat(wade, P.id, `HP${RUN}-`); await closeAll(clara, wade); }); test("two people on one packet see each other; leaving and locking drop out", async ({ browser }) => { const wes = await actor(browser, "sup.wes"); const andy = await actor(browser, "weld.andy"); const wesName = dcUser("sup.wes").display_name; const andyName = dcUser("weld.andy").display_name; // alone: nothing is drawn await openPacket(wes.page, P.id); await expect(callout(wes.page)).toHaveCount(0); // together: each sees the other with the role await openPacket(andy.page, P.id); await expect(callout(wes.page)).toContainText(L.alsoHere, { timeout: 10_000 }); await expect(callout(wes.page)).toContainText(`${andyName} (${L.roles.stall})`); await expect(callout(andy.page)).toContainText(L.alsoHere, { timeout: 10_000 }); await expect(callout(andy.page)).toContainText(`${wesName} (${L.roles.supervisor})`); await expect(callout(wes.page)).toHaveAttribute("role", "status"); await expect(callout(wes.page)).not.toContainText(wesName); // never yourself // weld.andy closes the page: gone within 10 s await andy.page.close(); await expect(callout(wes.page).filter({ hasText: andyName })).toHaveCount(0, { timeout: 10_000 }); // back again, then sup.wes locks: gone from weld.andy's callout andy.page = await andy.ctx.newPage(); await openPacket(andy.page, P.id); await expect(callout(andy.page)).toContainText(wesName, { timeout: 10_000 }); await api(wes, "post", "/api/auth/lock", undefined, { status: 204 }); await expect(callout(andy.page).filter({ hasText: wesName })).toHaveCount(0, { timeout: 10_000 }); await closeAll(wes, andy); }); });