230 lines
7.6 KiB
TypeScript
230 lines
7.6 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { test, expect } from "./fixtures";
|
|
|
|
/**
|
|
* onSave hook e2e (standalone-hardening 0005, save routing): a real user
|
|
* File→Save (Ctrl+S through the wx accelerator path) must fire
|
|
* window.kicadCollab.onSave(absPath) AFTER the bytes hit MEMFS — the signal the
|
|
* standalone app's save router (web/standalone/src/wasm/save-flow.ts) uses to
|
|
* persist saves out of the browser (API upload / local-disk write-back /
|
|
* download). The per-tool embind save helpers (kicadSaveDrawingSheet /
|
|
* kicadSaveSchematic / kicadSaveBoard) intentionally BYPASS the frame save
|
|
* chokepoint and must not fire it — they are test/materialize plumbing, not
|
|
* user saves.
|
|
*/
|
|
|
|
interface ToolCfg {
|
|
tool: string;
|
|
html: string;
|
|
ext: string;
|
|
saveFn: string;
|
|
/** Embind helper performing a genuine local edit (marks the doc modified, so
|
|
* the Save action is enabled when Ctrl+S arrives). */
|
|
modify: { fn: string; args: (string | number)[] };
|
|
fixture: string;
|
|
}
|
|
|
|
type Mod = Record<string, (...a: (string | number)[]) => unknown>;
|
|
type FS = {
|
|
mkdirTree(p: string): void;
|
|
writeFile(p: string, d: string): void;
|
|
readFile(p: string, o: { encoding: "utf8" }): string;
|
|
};
|
|
type HookWindow = Window & {
|
|
FS: FS;
|
|
Module: Mod;
|
|
kicadCollab?: Record<string, unknown>;
|
|
__savedPaths: string[];
|
|
};
|
|
|
|
const BOOT_TIMEOUT = 150000;
|
|
const NAME = "savehook";
|
|
|
|
/** Boot a fresh tool page and open the fixture from MEMFS (roundtrip.spec pattern). */
|
|
async function bootOpen(page: Page, cfg: ToolCfg): Promise<string> {
|
|
await page.goto(`/kicad/${cfg.html}`);
|
|
await expect(page.locator("#canvas")).toBeVisible({ timeout: BOOT_TIMEOUT });
|
|
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: BOOT_TIMEOUT });
|
|
await page.waitForFunction(
|
|
({ saveFn, modFn }) => {
|
|
const m = (window as unknown as { Module?: Mod }).Module;
|
|
return (
|
|
typeof m?.kicadOpenFile === "function" &&
|
|
typeof m?.[saveFn] === "function" &&
|
|
typeof m?.[modFn] === "function"
|
|
);
|
|
},
|
|
{ saveFn: cfg.saveFn, modFn: cfg.modify.fn },
|
|
{ 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 },
|
|
);
|
|
const abs = `/home/kicad/documents/${NAME}.${cfg.ext}`;
|
|
await page.evaluate(
|
|
({ content, abs }) => {
|
|
const w = window as unknown as HookWindow;
|
|
try {
|
|
w.FS.mkdirTree("/home/kicad/documents");
|
|
} catch {
|
|
/* exists */
|
|
}
|
|
w.FS.writeFile(abs, content);
|
|
w.Module.kicadOpenFile(abs);
|
|
},
|
|
{ content: cfg.fixture, abs },
|
|
);
|
|
await page.waitForTimeout(2000); // let the async OpenProjectFiles settle
|
|
return abs;
|
|
}
|
|
|
|
/** Click the drawing-area CENTER (clicking near the top edge hits the menubar —
|
|
* the wx canvas hosts the whole app UI). */
|
|
async function focusCanvas(page: Page): Promise<void> {
|
|
const box = await page.locator("#canvas").boundingBox();
|
|
if (box) await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
async function expectSaveHookFires(page: Page, cfg: ToolCfg): Promise<void> {
|
|
const abs = await bootOpen(page, cfg);
|
|
|
|
// Register the collector the way the standalone save router does (spread-merge
|
|
// so sibling bridge hooks survive).
|
|
await page.evaluate(() => {
|
|
const w = window as unknown as HookWindow;
|
|
w.__savedPaths = [];
|
|
w.kicadCollab = { ...w.kicadCollab, onSave: (p: string) => w.__savedPaths.push(p) };
|
|
});
|
|
|
|
// The embind save helper bypasses the frame chokepoint: NO hook.
|
|
await page.evaluate(
|
|
({ saveFn, ext }) => {
|
|
const w = window as unknown as HookWindow;
|
|
w.Module[saveFn](`/home/kicad/documents/helper_dump.${ext}`);
|
|
},
|
|
{ saveFn: cfg.saveFn, ext: cfg.ext },
|
|
);
|
|
expect(
|
|
await page.evaluate(() => (window as unknown as HookWindow).__savedPaths.length),
|
|
"embind save helper must not fire onSave",
|
|
).toBe(0);
|
|
|
|
// A genuine local edit (enables Save), then the user save: Ctrl+S.
|
|
await page.evaluate(
|
|
({ fn, args }) => (window as unknown as HookWindow).Module[fn](...args),
|
|
cfg.modify,
|
|
);
|
|
await page.waitForTimeout(500);
|
|
await focusCanvas(page);
|
|
await page.keyboard.press("Control+s");
|
|
|
|
await page.waitForFunction(
|
|
() => (window as unknown as HookWindow).__savedPaths.length > 0,
|
|
null,
|
|
{ timeout: 30000 },
|
|
);
|
|
const saved = await page.evaluate(() => (window as unknown as HookWindow).__savedPaths);
|
|
expect(saved[0], "onSave must carry the opened file's MEMFS path").toBe(abs);
|
|
|
|
// And the bytes were on MEMFS by the time the hook fired: the saved file
|
|
// carries the fixture's uuids (i.e. it is a real serialized document).
|
|
const text = await page.evaluate(
|
|
(p) => (window as unknown as HookWindow).FS.readFile(p, { encoding: "utf8" }),
|
|
saved[0],
|
|
);
|
|
expect(text).toContain('(uuid "');
|
|
}
|
|
|
|
// ── Fixtures (trimmed copies of roundtrip.spec's known-good documents) ────────
|
|
|
|
const PL: ToolCfg = {
|
|
tool: "pl_editor",
|
|
html: "pl_editor.html",
|
|
ext: "kicad_wks",
|
|
saveFn: "kicadSaveDrawingSheet",
|
|
modify: { fn: "kicadCollabTestAddText", args: ["save-hook", 30, 30] },
|
|
fixture: `(kicad_wks (version 20220228) (generator "pl_editor") (generator_version "9.0")
|
|
(setup (textsize 1.5 1.5)(linewidth 0.15)(textlinewidth 0.15)
|
|
(left_margin 10)(right_margin 10)(top_margin 10)(bottom_margin 10))
|
|
(rect (uuid "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") (name border) (start 0 0 ltcorner) (end 0 0 rbcorner))
|
|
)
|
|
`,
|
|
};
|
|
|
|
const SCH: ToolCfg = {
|
|
tool: "eeschema",
|
|
html: "eeschema.html",
|
|
ext: "kicad_sch",
|
|
saveFn: "kicadSaveSchematic",
|
|
modify: { fn: "kicadCollabTestMoveFirst", args: [2, 2] },
|
|
fixture: `(kicad_sch
|
|
(version 20250114)
|
|
(generator "eeschema")
|
|
(generator_version "9.0")
|
|
(uuid "11111111-1111-1111-1111-111111111111")
|
|
(paper "A4")
|
|
(lib_symbols)
|
|
(wire (pts (xy 50.8 50.8) (xy 101.6 50.8)) (stroke (width 0) (type default)) (uuid "22222222-0000-0000-0000-000000000001"))
|
|
(sheet_instances (path "/" (page "1")))
|
|
)
|
|
`,
|
|
};
|
|
|
|
const PCB: ToolCfg = {
|
|
tool: "pcbnew",
|
|
// pcbnew-collab.html seeds kicad_common.json so the first-run wizard is skipped.
|
|
html: "pcbnew-collab.html",
|
|
ext: "kicad_pcb",
|
|
saveFn: "kicadSaveBoard",
|
|
modify: { fn: "kicadCollabTestMoveFirst", args: [2, 2] },
|
|
fixture: `(kicad_pcb
|
|
(version 20241229)
|
|
(generator "pcbnew")
|
|
(generator_version "9.0")
|
|
(general (thickness 1.6))
|
|
(paper "A4")
|
|
(layers
|
|
(0 "F.Cu" signal)
|
|
(2 "B.Cu" signal)
|
|
(37 "F.SilkS" user)
|
|
(25 "Edge.Cuts" user)
|
|
)
|
|
(setup)
|
|
(net 0 "")
|
|
(footprint "TestLib:R"
|
|
(layer "F.Cu")
|
|
(uuid "66666666-0000-0000-0000-000000000001")
|
|
(at 100 100)
|
|
(attr smd)
|
|
(property "Reference" "R1" (at 0 -4.2 0) (layer "F.SilkS") (uuid "66666666-0000-0000-0000-0000000000aa") (effects (font (size 1 1) (thickness 0.15))))
|
|
)
|
|
)
|
|
`,
|
|
};
|
|
|
|
test.describe("user File→Save fires window.kicadCollab.onSave", () => {
|
|
// The heavy tools need well beyond the config's default per-test budget to boot.
|
|
test.describe.configure({ timeout: 300000 });
|
|
|
|
test("pl_editor: Ctrl+S → onSave with the saved MEMFS path", async ({ page, testLogger }) => {
|
|
void testLogger;
|
|
await expectSaveHookFires(page, PL);
|
|
});
|
|
|
|
test("eeschema: Ctrl+S → onSave with the saved MEMFS path", async ({ page, testLogger }) => {
|
|
void testLogger;
|
|
await expectSaveHookFires(page, SCH);
|
|
});
|
|
|
|
test("pcbnew: Ctrl+S → onSave with the saved MEMFS path", async ({ page, testLogger }) => {
|
|
void testLogger;
|
|
await expectSaveHookFires(page, PCB);
|
|
});
|
|
});
|