test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
This commit is contained in:
parent
80135a99ac
commit
4e92baf5d0
12 changed files with 2030 additions and 1 deletions
257
tests/kicad/ysync-repros-eeschema.spec.ts
Normal file
257
tests/kicad/ysync-repros-eeschema.spec.ts
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
/**
|
||||
* eeschema single-tab ysync coverage (docs in docs/features/ysync-review on
|
||||
* the ysync-review branch) — items-bridge.spec.ts driving style.
|
||||
*
|
||||
* Today this file holds the HEADLESS EMIT PROBE (plan phase C): whether a real
|
||||
* local commit emits the v2 items wire in the headless harness once the
|
||||
* listener IS registered. The legacy two-tab skip in eeschema-collab.spec.ts
|
||||
* cites a rationale its own single-page test documents as stale ("predated the
|
||||
* dyncall-shim fix — apply now works"); the EMIT half was never verified. If
|
||||
* this probe is green, the bug-01 eeschema two-tab repro in
|
||||
* ysync-two-tab.spec.ts stays a live test.fail; if red, it becomes test.fixme.
|
||||
*
|
||||
* The bug-04 edit matrix (rotate / field-text — plan phase D) lands here once
|
||||
* the C++ test hooks exist in the wasm build.
|
||||
*/
|
||||
|
||||
const WIRE1 = "22222222-0000-0000-0000-000000000001";
|
||||
const WIRE2 = "22222222-0000-0000-0000-000000000002";
|
||||
// A real placed symbol (embedded Device:R) — the bug-04 rotate / field-text
|
||||
// target: symbol rotation leaves GetPosition() unchanged, and fields are not
|
||||
// in screen->Items() at all.
|
||||
const SYM1 = "44444444-0000-0000-0000-000000000001";
|
||||
|
||||
const SAMPLE_SCH = `(kicad_sch
|
||||
\t(version 20250114)
|
||||
\t(generator "eeschema")
|
||||
\t(generator_version "9.0")
|
||||
\t(uuid "11111111-1111-1111-1111-111111111111")
|
||||
\t(paper "A4")
|
||||
\t(lib_symbols
|
||||
\t\t(symbol "Device:R" (pin_numbers (hide yes)) (pin_names (offset 0)) (exclude_from_sim no) (in_bom yes) (on_board yes)
|
||||
\t\t\t(property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27))))
|
||||
\t\t\t(property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27))))
|
||||
\t\t\t(symbol "R_0_1"
|
||||
\t\t\t\t(rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none)))
|
||||
\t\t\t)
|
||||
\t\t\t(symbol "R_1_1"
|
||||
\t\t\t\t(pin passive line (at 0 3.81 270) (length 1.27) (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
|
||||
\t\t\t\t(pin passive line (at 0 -3.81 90) (length 1.27) (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
|
||||
\t\t\t)
|
||||
\t\t)
|
||||
\t)
|
||||
\t(wire (pts (xy 50.8 50.8) (xy 101.6 50.8)) (stroke (width 0) (type default)) (uuid "${WIRE1}"))
|
||||
\t(wire (pts (xy 50.8 76.2) (xy 101.6 76.2)) (stroke (width 0) (type default)) (uuid "${WIRE2}"))
|
||||
\t(symbol (lib_id "Device:R") (at 63.5 63.5 0) (unit 1) (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)
|
||||
\t\t(uuid "${SYM1}")
|
||||
\t\t(property "Reference" "R1" (at 66.04 62.23 0) (effects (font (size 1.27 1.27)) (justify left)))
|
||||
\t\t(property "Value" "10k" (at 66.04 64.77 0) (effects (font (size 1.27 1.27)) (justify left)))
|
||||
\t\t(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
\t\t(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
\t\t(pin "1" (uuid "44444444-0000-0000-0000-0000000000a1"))
|
||||
\t\t(pin "2" (uuid "44444444-0000-0000-0000-0000000000a2"))
|
||||
\t\t(instances (project "rt" (path "/11111111-1111-1111-1111-111111111111" (reference "R1") (unit 1))))
|
||||
\t)
|
||||
\t(sheet_instances (path "/" (page "1")))
|
||||
)
|
||||
`;
|
||||
|
||||
type FS = {
|
||||
mkdirTree(p: string): void;
|
||||
writeFile(p: string, d: string): void;
|
||||
readFile(p: string, o: { encoding: "utf8" }): string;
|
||||
};
|
||||
type Mod = {
|
||||
kicadOpenFile(p: string): unknown;
|
||||
kicadCollabSnapshotItems(): string;
|
||||
kicadCollabApplyItems(j: string): unknown;
|
||||
kicadCollabTestMoveFirst(dx: number, dy: number): string;
|
||||
kicadCollabGetPos(id: string): string;
|
||||
kicadSaveSchematic(p: string): unknown;
|
||||
};
|
||||
|
||||
const BOOT_TIMEOUT = 150000;
|
||||
|
||||
function hasAbort(l: { consoleLogs: string[]; errors: string[] }): boolean {
|
||||
return [...l.consoleLogs, ...l.errors].some((s) => s.includes("Aborted("));
|
||||
}
|
||||
|
||||
async function bootOpen(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" &&
|
||||
typeof m?.kicadCollabApplyItems === "function" &&
|
||||
typeof m?.kicadCollabTestMoveFirst === "function" &&
|
||||
typeof m?.kicadSaveSchematic === "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((content) => {
|
||||
const w = window as unknown as { FS: FS; Module: Mod };
|
||||
try {
|
||||
w.FS.mkdirTree("/home/kicad/documents");
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
const p = "/home/kicad/documents/rt.kicad_sch";
|
||||
w.FS.writeFile(p, content);
|
||||
w.Module.kicadOpenFile(p);
|
||||
}, SAMPLE_SCH);
|
||||
}
|
||||
|
||||
test.describe("eeschema ysync repros (v2 items wire, single tab)", () => {
|
||||
test.describe.configure({ timeout: 420000 });
|
||||
|
||||
test("control: a local move emits the v2 items wire (HEADLESS EMIT PROBE)", async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
await bootOpen(page);
|
||||
// snapshotItems: registers the SCHEMATIC_LISTENER (ensureBridge) +
|
||||
// baselines the differ — what seed()'s non-file-seed branches rely on.
|
||||
await page.evaluate(() => window.Module.kicadCollabSnapshotItems());
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { __items: string[] }).__items = [];
|
||||
(window as unknown as { kicadCollab: object }).kicadCollab = {
|
||||
onItems: (j: string) => (window as unknown as { __items: string[] }).__items.push(j),
|
||||
};
|
||||
});
|
||||
|
||||
const movedId = (await page.evaluate(() =>
|
||||
window.Module.kicadCollabTestMoveFirst(200000, 0),
|
||||
)) as string;
|
||||
expect(movedId).toMatch(/[0-9a-f-]{36}/);
|
||||
|
||||
// listener → scheduleFlush → flushDiff → emitItems: the moved item's uuid
|
||||
// must appear in an emitted wire. Phase-C gate for the eeschema bug-01
|
||||
// two-tab repro and the phase-D bug-04 matrix.
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
await page.evaluate(
|
||||
() => (window as unknown as { __items: string[] }).__items.join("\n"),
|
||||
),
|
||||
{ timeout: 20000, intervals: [400] },
|
||||
)
|
||||
.toContain(movedId);
|
||||
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||
});
|
||||
|
||||
// ── Bug 04 matrix — edits invisible to the scalar-projection differ ────────
|
||||
// 04-bug-lossy-change-detection.md. Same shape as the pcbnew matrix: real
|
||||
// commit via a repro hook → green precondition the edit LANDED → expected-
|
||||
// fail that it EMITTED. Gated on the wasm build carrying the hooks.
|
||||
|
||||
function saveRead(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const w = window as unknown as { FS: FS; Module: Mod };
|
||||
const out = "/home/kicad/documents/probe.kicad_sch";
|
||||
w.Module.kicadSaveSchematic(out);
|
||||
return w.FS.readFile(out, { encoding: "utf8" });
|
||||
});
|
||||
}
|
||||
|
||||
function emittedWires(page: Page): Promise<string> {
|
||||
return page.evaluate(() =>
|
||||
(window as unknown as { __items: string[] }).__items.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
/** Baseline + capture + hook-presence guard (returns false on stale wasm). */
|
||||
async function armed(page: Page, hook: string): Promise<boolean> {
|
||||
await bootOpen(page);
|
||||
const has = await page.evaluate(
|
||||
(h) => typeof (window as unknown as { Module: Record<string, unknown> }).Module[h] === "function",
|
||||
hook,
|
||||
);
|
||||
if (!has) return false;
|
||||
await page.evaluate(() => window.Module.kicadCollabSnapshotItems());
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { __items: string[] }).__items = [];
|
||||
(window as unknown as { kicadCollab: object }).kicadCollab = {
|
||||
onItems: (j: string) => (window as unknown as { __items: string[] }).__items.push(j),
|
||||
};
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
test("an in-place symbol rotation reaches the wire", async ({ page, testLogger }) => {
|
||||
test.fail(); // bug 04 — GetPosition() unchanged; no orientation in the json
|
||||
|
||||
const ok = await armed(page, "kicadCollabTestRotateItem");
|
||||
test.skip(!ok, "wasm build predates the ysync repro hooks");
|
||||
|
||||
const queued = await page.evaluate(
|
||||
(id) =>
|
||||
(window as unknown as { Module: { kicadCollabTestRotateItem(i: string, d: number): boolean } })
|
||||
.Module.kicadCollabTestRotateItem(id, 90),
|
||||
SYM1,
|
||||
);
|
||||
expect(queued, "rotate hook resolved the symbol").toBe(true);
|
||||
|
||||
// Green precondition: the rotation LANDED (the saved symbol's placement
|
||||
// gained an angle).
|
||||
await expect
|
||||
.poll(async () => /\(at 63.5 63.5 (90|180|270)\)/.test(await saveRead(page)), {
|
||||
timeout: 15000,
|
||||
intervals: [400],
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// CORRECT: the rotation is broadcast. TODAY: the scalar diff is empty.
|
||||
await expect
|
||||
.poll(async () => await emittedWires(page), { timeout: 8000, intervals: [400] })
|
||||
.toContain(SYM1);
|
||||
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||
});
|
||||
|
||||
test("a symbol Value field edit reaches the wire", async ({ page, testLogger }) => {
|
||||
test.fail(); // bug 04 — fields are invisible to the snapshot entirely
|
||||
|
||||
const ok = await armed(page, "kicadCollabTestSetFieldText");
|
||||
test.skip(!ok, "wasm build predates the ysync repro hooks");
|
||||
|
||||
const queued = await page.evaluate(
|
||||
(id) =>
|
||||
(window as unknown as { Module: { kicadCollabTestSetFieldText(i: string, t: string): boolean } })
|
||||
.Module.kicadCollabTestSetFieldText(id, "22k"),
|
||||
SYM1,
|
||||
);
|
||||
expect(queued, "field hook resolved the symbol").toBe(true);
|
||||
|
||||
// Green precondition: the edit LANDED in the model.
|
||||
await expect
|
||||
.poll(async () => (await saveRead(page)).includes(`"22k"`), {
|
||||
timeout: 15000,
|
||||
intervals: [400],
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// CORRECT: the most common schematic edit after moving things is
|
||||
// broadcast. TODAY: nothing emits — the value edit stays local forever.
|
||||
await expect
|
||||
.poll(async () => await emittedWires(page), { timeout: 8000, intervals: [400] })
|
||||
.toContain(SYM1);
|
||||
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue