import { test, expect, type Page } from '@playwright/test'; /** * Cross-app selection e2e (collab-presence 0006): one tab on the demo * SCHEMATIC and one on the demo BOARD of the same project share the * project-wide presence room over the default BroadcastChannel provider. * Selecting a symbol in eeschema must ghost-highlight the linked footprint in * pcbnew (footprint `(path …)` tail = symbol uuid — the demo fixtures are * linked), and vice versa. Assertions use the deterministic * `kicadCollabTestGetCrossMapped()` probe (what the ghost render draws from) * rather than pixel diffs — the render path itself is covered by the * tests/kicad specs. * * This is also the single-user cross-probe test by construction: both tabs * run as the same `?user=` in one browser context and still map each other * (cross-app peers are keyed by client, not user). */ const SCOPE = 'default'; type Mod = { kicadCollabTestSelectComponent(): string; kicadCollabTestClearSelection(): boolean; kicadCollabTestGetCrossMapped(): string; kicadCollabGetSelectionFull(): string; }; type W = { Module: Mod }; async function bootTool( page: Page, route: string, titleRe: RegExp, user: string, ): Promise { await page.goto(`/${SCOPE}/projects/demo/${route}?user=${user}`); await expect(page.locator('#canvas')).toBeVisible({ timeout: 120000 }); await expect .poll(() => page.title(), { message: `${route}: editor never reached the expected title`, timeout: 120000, intervals: [1000], }) .toMatch(titleRe); await page.waitForFunction( () => { const m = (window as unknown as Partial).Module; return ( typeof m?.kicadCollabTestSelectComponent === 'function' && typeof m?.kicadCollabTestGetCrossMapped === 'function' ); }, null, { timeout: 60000 }, ); } const crossMapped = (page: Page) => page.evaluate(() => JSON.parse((window as unknown as W).Module.kicadCollabTestGetCrossMapped()), ); test('selecting a symbol in eeschema ghost-highlights the footprint in pcbnew, and back', async ({ page, context, }) => { test.setTimeout(480000); // two full editor boots (eeschema + pcbnew wasm) const sch = page; await bootTool(sch, 'demo.kicad_sch', /demo — Schematic Editor/i, 'alice'); const pcb = await context.newPage(); await bootTool(pcb, 'demo.kicad_pcb', /demo — PCB Editor/i, 'bob'); // ── eeschema → pcbnew ────────────────────────────────────────────────────── const symId = await sch.evaluate(() => (window as unknown as W).Module.kicadCollabTestSelectComponent(), ); expect(symId, 'demo schematic should contain a symbol').toBeTruthy(); // The board tab maps the symbol to ≥1 footprint (reused sheets map to more). await expect .poll(() => crossMapped(pcb), { timeout: 20000, message: 'pcbnew never received/mapped the eeschema selection', }) .not.toEqual([]); // The selection emit carried the symbol uuid across (sanity on the wire). const schSel = await sch.evaluate(() => JSON.parse((window as unknown as W).Module.kicadCollabGetSelectionFull()), ); expect(schSel.uuids).toContain(symId); // Clearing the schematic selection clears the board ghosts. await sch.evaluate(() => (window as unknown as W).Module.kicadCollabTestClearSelection(), ); await expect .poll(() => crossMapped(pcb), { timeout: 20000 }) .toEqual([]); // ── pcbnew → eeschema ────────────────────────────────────────────────────── const fpId = await pcb.evaluate(() => (window as unknown as W).Module.kicadCollabTestSelectComponent(), ); expect(fpId, 'demo board should contain a footprint').toBeTruthy(); // The footprint's path tail resolves to a symbol on the schematic's sheet. await expect .poll(() => crossMapped(sch), { timeout: 20000, message: 'eeschema never received/mapped the pcbnew selection', }) .not.toEqual([]); // …and the emit carried the footprint's schematic path. const pcbSel = await pcb.evaluate(() => JSON.parse((window as unknown as W).Module.kicadCollabGetSelectionFull()), ); expect(pcbSel.uuids).toContain(fpId); expect(pcbSel.fpPaths.length).toBeGreaterThan(0); await pcb.evaluate(() => (window as unknown as W).Module.kicadCollabTestClearSelection(), ); await expect .poll(() => crossMapped(sch), { timeout: 20000 }) .toEqual([]); });