diff --git a/kicad b/kicad index 933c2fd..9053a26 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 933c2fda02712169fd418f70225d2637f0b8c2f8 +Subproject commit 9053a260c7f497f3df05c36090c2f5f768bae453 diff --git a/tests/kicad/pl_editor-uuid.spec.ts b/tests/kicad/pl_editor-uuid.spec.ts new file mode 100644 index 0000000..b2098e9 --- /dev/null +++ b/tests/kicad/pl_editor-uuid.spec.ts @@ -0,0 +1,152 @@ +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 { + 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); + }); +}); diff --git a/wasm/bindings/pl_editor_embind.cpp b/wasm/bindings/pl_editor_embind.cpp index e63f84f..06bfc5e 100644 --- a/wasm/bindings/pl_editor_embind.cpp +++ b/wasm/bindings/pl_editor_embind.cpp @@ -13,6 +13,7 @@ #include #include #include +#include using namespace emscripten; @@ -37,8 +38,20 @@ bool kicadOpenFile( std::string path ) std::vector( 1, wxString::FromUTF8( path.c_str() ) ) ); } +// Programmatically save the in-memory drawing sheet to a .kicad_wks file, without +// driving the Save As dialog. pl_editor edits the process-wide singleton +// DS_DATA_MODEL::GetTheInstance(), so this serializes exactly what the editor has +// loaded — letting a test read the file back from MEMFS and assert the (uuid …) +// round-trip / backfill. (Also a building block for the collab bridge's later +// materialize-to-file path.) +void kicadSaveDrawingSheet( std::string path ) +{ + DS_DATA_MODEL::GetTheInstance().Save( wxString::FromUTF8( path.c_str() ) ); +} + EMSCRIPTEN_BINDINGS(pl_editor) { // Programmatic file open (preferred over UI automation from the web app). function("kicadOpenFile", &kicadOpenFile); + function("kicadSaveDrawingSheet", &kicadSaveDrawingSheet); } #endif