Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
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<void> {
|
|
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<W>).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([]);
|
|
});
|