pcbjam/tests/kicad/eeschema-subschema.spec.ts

148 lines
5.4 KiB
TypeScript
Raw Permalink Normal View History

import type { Page } from "@playwright/test";
import { test, expect } from "./fixtures";
/**
* Subschema (hierarchical sheet) collab scoping the Phase-0 C++ change.
*
* A hierarchical design references a child `.kicad_sch` from the root via a (sheet )
* symbol; opening the root loads BOTH screens into memory. Each `.kicad_sch` is its own
* collab room, so `kicadCollabSnapshotItems()` (which feeds the active room) must return
* ONLY the active screen's items NOT the whole hierarchy. Before Phase 0 it iterated
* `Schematic().Hierarchy()` and would have leaked the child's items into the root's room.
*
* Headless scope: this asserts the snapshot is scoped to the root (active) screen on
* load. Driving sheet navigation + the onSheetChanged hook needs the real wx UI (the
* same reason eeschema-collab's two-tab test is skipped) and is verified in the app.
*/
type Mod = {
kicadOpenFile(p: string): unknown;
kicadCollabSnapshotItems(): string;
} & Record<string, (...a: never[]) => unknown>;
type FS = {
mkdirTree(p: string): void;
writeFile(p: string, d: string): void;
};
const BOOT_TIMEOUT = 150000;
const ROOT_WIRE_UUID = "1aaaaaaa-0000-0000-0000-000000000001";
const SHEET_SYMBOL_UUID = "5ee70000-0000-0000-0000-000000000001";
const CHILD_WIRE_UUID = "2ccccccc-0000-0000-0000-000000000001";
const ROOT_SCH = `(kicad_sch
(version 20250114)
(generator "eeschema")
(generator_version "9.0")
(uuid "10000000-0000-0000-0000-000000000000")
(paper "A4")
(lib_symbols)
(wire (pts (xy 50.8 50.8) (xy 101.6 50.8)) (stroke (width 0) (type default)) (uuid "${ROOT_WIRE_UUID}"))
(sheet (at 127 50.8) (size 20 20)
(stroke (width 0.1524) (type solid))
(fill (color 0 0 0 0.0000))
(uuid "${SHEET_SYMBOL_UUID}")
(property "Sheetname" "child" (at 127 50 0) (effects (font (size 1.27 1.27)) (justify left bottom)))
(property "Sheetfile" "child.kicad_sch" (at 127 71 0) (effects (font (size 1.27 1.27)) (justify left top)))
(instances (project "rt" (path "/10000000-0000-0000-0000-000000000000" (page "2"))))
)
(sheet_instances (path "/" (page "1")))
)
`;
const CHILD_SCH = `(kicad_sch
(version 20250114)
(generator "eeschema")
(generator_version "9.0")
(uuid "20000000-0000-0000-0000-000000000000")
(paper "A4")
(lib_symbols)
(wire (pts (xy 25.4 25.4) (xy 76.2 25.4)) (stroke (width 0) (type default)) (uuid "${CHILD_WIRE_UUID}"))
(sheet_instances (path "/" (page "1")))
)
`;
function hasAbort(l: { consoleLogs: string[]; errors: string[] }): boolean {
return [...l.consoleLogs, ...l.errors].some((s) => s.includes("Aborted("));
}
async function bootOpenHierarchy(page: Page): Promise<void> {
await page.goto("/kicad/eeschema.html");
await expect(page.locator("#canvas")).toBeVisible({ timeout: BOOT_TIMEOUT });
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: BOOT_TIMEOUT });
await page.waitForFunction(
() => {
const m = (window as unknown as { Module?: Mod }).Module;
return (
typeof m?.kicadOpenFile === "function" &&
typeof m?.kicadCollabSnapshotItems === "function"
);
},
null,
{ timeout: BOOT_TIMEOUT },
);
await page.waitForFunction(
() =>
!!window.wxElementRegistry &&
window.wxElementRegistry
.findAll({ visible: true })
.some((e) => /Frame$/.test(e.typeName) || (e.name || "").endsWith("Frame")),
null,
{ timeout: BOOT_TIMEOUT },
);
await page.evaluate(
({ root, child }) => {
const w = window as unknown as { FS: FS; Module: Mod };
try {
w.FS.mkdirTree("/home/kicad/documents");
} catch {
/* exists */
}
// Write BOTH sheets so the root's (sheet … Sheetfile "child.kicad_sch") resolves
// and eeschema loads the child screen alongside the root.
w.FS.writeFile("/home/kicad/documents/child.kicad_sch", child);
w.FS.writeFile("/home/kicad/documents/root.kicad_sch", root);
w.Module.kicadOpenFile("/home/kicad/documents/root.kicad_sch");
},
{ root: ROOT_SCH, child: CHILD_SCH },
);
}
test.describe("eeschema subschema (hierarchical sheet) collab scoping", () => {
test.describe.configure({ timeout: 420000 });
test("snapshot covers ONLY the active (root) screen, not the child sheet", async ({
page,
testLogger,
}) => {
await bootOpenHierarchy(page);
// Poll: the root snapshot must include the root's own items but NOT the child's.
await expect
.poll(
async () => {
const snap = JSON.parse(
await page.evaluate(() => window.Module.kicadCollabSnapshotItems()),
) as { added: Array<{ sexpr: string }> };
return snap.added.map((w) => w.sexpr).join("\n");
},
{ timeout: 30000, intervals: [500] },
)
.toContain(ROOT_WIRE_UUID);
const blobs = JSON.parse(
await page.evaluate(() => window.Module.kicadCollabSnapshotItems()),
) as { added: Array<{ sexpr: string }> };
const allBlobs = blobs.added.map((w) => w.sexpr).join("\n");
// Root items present (the wire + the sheet symbol live on the root screen)…
expect(allBlobs, "root wire in snapshot").toContain(ROOT_WIRE_UUID);
expect(allBlobs, "sheet symbol in snapshot").toContain(SHEET_SYMBOL_UUID);
// …but the CHILD sheet's items must NOT leak into the root's room (Phase 0).
expect(allBlobs, "child wire must NOT be in the root snapshot").not.toContain(
CHILD_WIRE_UUID,
);
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
});
});