pcbjam/tests/kicad/pl_editor-uuid.spec.ts
Gergő Törcsvári 7d49007473
feat(pl_editor): add KIID m_Uuid + (uuid) to .kicad_wks format (yjs-bridge commit 1)
Bump kicad submodule to the per-item uuid identity change, plus the wasm/test
infra to verify it:

- wasm/bindings/pl_editor_embind.cpp: test-only kicadSaveDrawingSheet(path) hook
  that serializes the singleton DS_DATA_MODEL to MEMFS (also a building block for
  the bridge's later materialize-to-file path)
- tests/kicad/pl_editor-uuid.spec.ts: open->save->read-back e2e proving (uuid …)
  backfill (4 distinct uuids) and load->save round-trip preservation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:56:38 +02:00

152 lines
6.4 KiB
TypeScript

import { test, expect } from './fixtures';
/**
* pl_editor (.kicad_wks) per-item `(uuid …)` round-trip + backfill.
*
* Guards Yjs-bridge commit 1 (identity/format): every DS_DATA_ITEM now carries a
* stable KIID m_Uuid that is serialized as `(uuid …)` via the shared
* KICAD_FORMAT::FormatUuid, parsed back per item, and — when absent from a
* legacy/upstream file — backfilled (default-constructed random KIID) on load so
* the first save upgrades the file in place.
*
* Strategy mirrors pl_editor-load.spec.ts: write a .kicad_wks into MEMFS, open it
* with Module.kicadOpenFile(), then serialize the in-memory model back with the
* test-only Module.kicadSaveDrawingSheet() hook and read the output from MEMFS.
* - backfill: a uuid-less file gains one (uuid …) per item after save.
* - round-trip: a file that already has uuids keeps the *exact same* strings.
*/
// Legacy file: no (uuid …) tokens. 1 line + 1 rect + 1 tbtext + 1 polygon = 4 items.
const LEGACY_WKS = `(page_layout
(setup (textsize 1.5 1.5)(linewidth 0.15)(textlinewidth 0.15)
(left_margin 10)(right_margin 10)(top_margin 10)(bottom_margin 10))
(line (name segment) (start 20 20 ltcorner) (end 60 20 ltcorner))
(rect (name border:Rect) (start 0 0 ltcorner) (end 0 0 rbcorner) (comment "page border"))
(tbtext "Backfill me" (name title) (pos 100 20) (font (size 2.5 2.5) bold))
(polygon (name logo) (pos 30 30 ltcorner) (pts (xy 0 0)(xy 5 0)(xy 5 5)(xy 0 5)))
)
`;
// Already-uuid'd file: each item carries an explicit, known uuid. These exact
// strings must survive load → save unchanged (parser reads them; writer re-emits).
const U_LINE = '11111111-1111-1111-1111-111111111111';
const U_RECT = '22222222-2222-2222-2222-222222222222';
const U_TEXT = '33333333-3333-3333-3333-333333333333';
const U_POLY = '44444444-4444-4444-4444-444444444444';
const UUID_WKS = `(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))
(line (uuid "${U_LINE}") (name segment) (start 20 20 ltcorner) (end 60 20 ltcorner))
(rect (uuid "${U_RECT}") (name border:Rect) (start 0 0 ltcorner) (end 0 0 rbcorner))
(tbtext "Keep my uuid" (uuid "${U_TEXT}") (name title) (pos 100 20) (font (size 2.5 2.5) bold))
(polygon (uuid "${U_POLY}") (name logo) (pos 30 30 ltcorner) (pts (xy 0 0)(xy 5 0)(xy 5 5)(xy 0 5)))
)
`;
type EmscriptenFS = {
mkdirTree(path: string): void;
writeFile(path: string, data: string): void;
readFile(path: string, opts: { encoding: 'utf8' }): string;
};
type KicadModule = {
kicadOpenFile(path: string): unknown;
kicadSaveDrawingSheet(path: string): unknown;
};
function hasAbort(testLogger: { consoleLogs: string[]; errors: string[] }): boolean {
return [...testLogger.consoleLogs, ...testLogger.errors].some((l) => l.includes('Aborted('));
}
/** Open `content` in the editor, save it back, return the serialized output. */
async function openThenSave(
page: import('@playwright/test').Page,
name: string,
content: string,
): Promise<string> {
const inPath = `/home/kicad/documents/${name}.kicad_wks`;
await page.evaluate(
({ inPath, content }) => {
const w = window as unknown as { FS: EmscriptenFS; Module: KicadModule };
const dir = '/home/kicad/documents';
try {
w.FS.mkdirTree(dir);
} catch {
/* already exists */
}
w.FS.writeFile(inPath, content);
w.Module.kicadOpenFile(inPath);
},
{ inPath, content },
);
// Title flips to the opened file once the load completes.
await expect
.poll(async () => page.title(), { timeout: 30000, intervals: [500] })
.toMatch(new RegExp(name, 'i'));
return page.evaluate((name) => {
const w = window as unknown as { FS: EmscriptenFS; Module: KicadModule };
const outPath = `/home/kicad/documents/${name}-out.kicad_wks`;
w.Module.kicadSaveDrawingSheet(outPath);
return w.FS.readFile(outPath, { encoding: 'utf8' });
}, name);
}
test.describe('pl_editor .kicad_wks uuid identity', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/kicad/pl_editor.html');
await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
await page.waitForFunction(
() => {
const m = (window as unknown as { Module?: KicadModule }).Module;
return (
typeof m?.kicadOpenFile === 'function' &&
typeof m?.kicadSaveDrawingSheet === 'function'
);
},
null,
{ timeout: 90000 },
);
await page.waitForFunction(
() =>
!!window.wxElementRegistry &&
window.wxElementRegistry
.findAll({ visible: true })
.some((e) => /Frame$/.test(e.typeName) || (e.name || '').endsWith('Frame')),
null,
{ timeout: 90000 },
);
});
test('a uuid-less file is backfilled: one (uuid …) per item on save', async ({
page,
testLogger,
}) => {
const out = await openThenSave(page, 'legacy', LEGACY_WKS);
// 4 items in, so 4 freshly-minted uuids out.
const uuids = [...out.matchAll(/\(uuid\s+"([0-9a-fA-F-]{36})"\)/g)].map((m) => m[1]);
expect(uuids, `expected 4 backfilled uuids, output was:\n${out}`).toHaveLength(4);
// All non-nil and distinct (random backfill, not shared/zero).
const NIL = '00000000-0000-0000-0000-000000000000';
for (const u of uuids) expect(u).not.toBe(NIL);
expect(new Set(uuids).size, 'backfilled uuids must be distinct').toBe(4);
expect(hasAbort(testLogger), 'no WASM abort').toBe(false);
});
test('existing uuids survive load → save unchanged (round-trip)', async ({
page,
testLogger,
}) => {
const out = await openThenSave(page, 'withuuid', UUID_WKS);
for (const u of [U_LINE, U_RECT, U_TEXT, U_POLY]) {
expect(out, `uuid ${u} must be preserved through load/save`).toContain(`(uuid "${u}")`);
}
expect(hasAbort(testLogger), 'no WASM abort').toBe(false);
});
});