import { test, expect, type Page } from '@playwright/test'; /** * Selection soft-locks e2e (collab-presence 0007): two tabs of the real app * on the SAME board share the per-file room — one tab's live selection must * soft-lock those items for the other, and an overlapping hold (both grab the * same item inside the propagation window — simulated with the programmatic * select, which bypasses the acquisition veto exactly like a real race) * resolves by the deterministic (user.id, clientID) tiebreak: alice keeps, * bob's client releases. */ const SCOPE = 'default'; type Mod = { kicadCollabGetSelection(): string; kicadCollabTestGetLocked(): string; kicadCollabTestSelectComponent(): string; kicadCollabTestSelectByUuid(uuid: string): boolean; kicadCollabTestClearSelection(): boolean; }; type W = { Module: Mod }; async function bootBoard(page: Page, user: string): Promise { await page.goto(`/${SCOPE}/projects/demo/demo.kicad_pcb?user=${user}`); await expect(page.locator('#canvas')).toBeVisible({ timeout: 120000 }); await expect .poll(() => page.title(), { message: `${user}: board editor never reached the expected title`, timeout: 120000, intervals: [1000], }) .toMatch(/demo — PCB Editor/i); await page.waitForFunction( () => typeof (window as unknown as Partial).Module?.kicadCollabTestGetLocked === 'function', null, { timeout: 60000 }, ); } const selection = (page: Page) => page.evaluate(() => JSON.parse((window as unknown as W).Module.kicadCollabGetSelection()), ); const locked = (page: Page) => page.evaluate(() => JSON.parse((window as unknown as W).Module.kicadCollabTestGetLocked()), ); test('a peer selection locks the item; overlapping holds tiebreak deterministically', async ({ page, context, }) => { test.setTimeout(480000); // two full board boots const alice = page; await bootBoard(alice, 'alice'); const bob = await context.newPage(); await bootBoard(bob, 'bob'); // ── lock propagation ─────────────────────────────────────────────────────── const fpId = await alice.evaluate(() => (window as unknown as W).Module.kicadCollabTestSelectComponent(), ); expect(fpId, 'demo board should contain a footprint').toBeTruthy(); await expect .poll(() => locked(bob), { timeout: 20000, message: "bob never saw alice's selection as a lock", }) .toEqual([{ uuid: fpId, name: 'alice' }]); // The holder's own tab is NOT locked against itself. await expect.poll(() => locked(alice), { timeout: 20000 }).toEqual([]); // ── overlapping hold → tiebreak ──────────────────────────────────────────── // bob grabs the SAME footprint programmatically (the race-window simulation: // AddItemToSel bypasses the acquisition veto, like two grabs inside the // awareness propagation window). By uuid — bob's ysync-materialized board // need not iterate footprints in alice's parse order, so "the first // footprint" is not a cross-tab invariant. const bobPick = await bob.evaluate( (uuid) => (window as unknown as W).Module.kicadCollabTestSelectByUuid(uuid), fpId, ); expect(bobPick, "bob must resolve alice's footprint by uuid").toBe(true); // alice ("alice" < "bob") keeps it; bob's client releases it. await expect .poll(() => selection(bob), { timeout: 20000, message: "bob's losing hold was never released", }) .toEqual([]); expect(await selection(alice)).toContain(fpId); // ── unlock on clear ──────────────────────────────────────────────────────── await alice.evaluate(() => (window as unknown as W).Module.kicadCollabTestClearSelection(), ); await expect.poll(() => locked(bob), { timeout: 20000 }).toEqual([]); // Now bob's grab sticks, and it locks the item for alice (same uuid — the // assertions below compare against fpId). await bob.evaluate( (uuid) => (window as unknown as W).Module.kicadCollabTestSelectByUuid(uuid), fpId, ); await expect .poll(() => selection(bob), { timeout: 20000 }) .toContain(fpId); await expect .poll(() => locked(alice), { timeout: 20000 }) .toEqual([{ uuid: fpId, name: 'bob' }]); });