pcbjam/tests/kicad/save-cmd-key.spec.ts
Gergő Törcsvári 866db5888c
libs 0017: sync overrides indexed + stale-lib session menu + Cmd+S DOM-focus fix + WasmTool split
- kicadLibsFootprintUsage + kicadUpdateFromLibrary embinds (result via
  pcbjam:lib-update-done — runOnCoroutine is deferred)
- standalone: stale-lib FAB triangle + session-menu Update-from-library row,
  save busy notice names the item, footprint placed-usage in the toast
- WasmTool.tsx split: module helpers → components/wasm-tool/
- specs: save-cmd-key (Meta+S, mac UA), fpedit-cmd-save (DOM-focus repro)
- wxwidgets → cdd5a5c (wxDomBlurActive)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-25 17:58:54 +02:00

187 lines
6.2 KiB
TypeScript

import type { Page } from "@playwright/test";
import { test, expect } from "./fixtures";
/**
* Cmd+S (Meta+S — the Mac save chord) must save exactly like Ctrl+S does.
*
* save-hook.spec pins Ctrl+S → window.kicadCollab.onSave for every frame; every
* other spec in the tree presses "Control+s" too, so the Meta chord had no
* coverage at all. A Mac user reported "Cmd+S didn't save, the Save icon did"
* (libs 0017). This spec is the red side of that report: same boot / edit /
* hook recipe as save-hook.spec, chord swapped for Meta+S. A Ctrl+S control
* run in the same session guards against blaming the chord for a boot
* problem.
*
* wx side: keyboard.cpp SetKeyboardModifiers maps metaKey → ControlDown only
* when wxGetOsVersion() reports a Mac (wx.js platformInfo from the UA), and
* app.cpp TranslateMenuAccel matches "\tCtrl+S" on ControlDown — so the Meta
* chord has two places to fall through.
*/
interface ToolCfg {
html: string;
ext: string;
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;
};
type HookWindow = Window & {
FS: FS;
Module: Mod;
kicadCollab?: Record<string, unknown>;
__savedPaths: string[];
};
const BOOT_TIMEOUT = 150000;
const NAME = "savecmd";
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(
(modFn) => {
const m = (window as unknown as { Module?: Mod }).Module;
return typeof m?.kicadOpenFile === "function" && typeof m?.[modFn] === "function";
},
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 expect.poll(() => page.title(), { timeout: BOOT_TIMEOUT, intervals: [300] }).toMatch(new RegExp(NAME, "i"));
return abs;
}
async function focusCanvas(page: Page): Promise<void> {
const box = await page.locator("#canvas").boundingBox();
expect(box, "#canvas has a bounding box").not.toBeNull();
await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2);
await page.waitForTimeout(300); // eslint-disable-line -- focus click settle, no JS signal
}
/** Edit, press `chord`, return the number of onSave calls it produced within 10 s. */
async function savesAfter(page: Page, cfg: ToolCfg, chord: string): Promise<number> {
await page.evaluate(() => {
(window as unknown as HookWindow).__savedPaths = [];
});
await page.evaluate(
({ fn, args }) => (window as unknown as HookWindow).Module[fn](...args),
cfg.modify,
);
await page.waitForTimeout(500); // eslint-disable-line -- dirty flag settle (save-hook.spec)
await focusCanvas(page);
await page.keyboard.press(chord);
await page
.waitForFunction(() => (window as unknown as HookWindow).__savedPaths.length > 0, null, {
timeout: 10000,
})
.catch(() => undefined);
return page.evaluate(() => (window as unknown as HookWindow).__savedPaths.length);
}
async function expectMetaSaves(page: Page, cfg: ToolCfg): Promise<void> {
await bootOpen(page, cfg);
await page.evaluate(() => {
const w = window as unknown as HookWindow;
w.__savedPaths = [];
w.kicadCollab = { ...w.kicadCollab, onSave: (p: string) => w.__savedPaths.push(p) };
});
// Control: the Ctrl chord saves in this very session (else the boot is the
// problem, not the chord).
expect(await savesAfter(page, cfg, "Control+s"), "Ctrl+S control save").toBeGreaterThan(0);
// The claim: Cmd+S saves too.
expect(await savesAfter(page, cfg, "Meta+s"), "Meta+S (Cmd+S) save").toBeGreaterThan(0);
}
const SCH: ToolCfg = {
html: "eeschema.html",
ext: "kicad_sch",
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 = {
html: "pcbnew-collab.html",
ext: "kicad_pcb",
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("Cmd+S (Meta+S) saves like Ctrl+S", () => {
test.describe.configure({ timeout: 300000 });
// The Playwright device presets carry a WINDOWS user agent; wx.js derives the
// OS from the UA and only maps the Meta key to wx's ControlDown on a Mac.
// Pin a macOS UA so the chord is judged the way a Mac browser sends it.
test.use({
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:153.0) Gecko/20100101 Firefox/153.0",
});
test("eeschema: Meta+S → onSave", async ({ page, testLogger }) => {
void testLogger;
await expectMetaSaves(page, SCH);
});
test("pcbnew: Meta+S → onSave", async ({ page, testLogger }) => {
void testLogger;
await expectMetaSaves(page, PCB);
});
});