pcbjam/tests/web/fpedit-from-eeschema.spec.ts
Gergő Törcsvári f92266fcee
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.

- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
  CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
  inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
  (instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
  is invalid file content but load-bearing on decode — without it the
  parser starts at m_requiredVersion=0 and stamps (hide yes) on every
  mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
  keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
  legitimately; excluded from counts, report-worthiness and dedupe hashes.
  Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
  FACE_PCB starts lazily there and never preloaded its libraries.

Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:32:04 +02:00

138 lines
5.8 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test';
import { clickByTooltip, stableShot, waitForWxApp } from '../e2e/utils/element-tracker';
/**
* Footprint Editor opened FROM a schematic session must show its libraries.
*
* Regression guard for the ysync-0010 §4 bug: `single_top.cpp` preloads libraries
* only for the face the app booted into. Booting `--frame=fpedit` (what
* `-/footprint_editor` does, and what every other fp-editor spec exercises)
* preloads FACE_PCB and works. But the eeschema toolbar button routes through
* `Kiway().Player( FRAME_FOOTPRINT_EDITOR )`, which starts FACE_PCB lazily at
* click time and never preloads — so nothing had walked the footprint libraries,
* `LIBRARY_MANAGER_ADAPTER::GetLibraryNames()` reported only rows whose status is
* LOADED (i.e. none), and `FP_TREE_SYNCHRONIZING_ADAPTER::Sync` skipped
* everything. The editor opened with a completely empty library tree.
*
* Fixed by `AsyncLoad()` + `BlockUntilLoaded()` in
* `FOOTPRINT_EDIT_FRAME::initLibraryTree()`.
*
* Two things this spec must NOT do, both learned the hard way:
*
* - Don't assert on `document.title`. It stays "… — Schematic Editor" even after
* a second editor frame opens (verified with Symbol Editor too), so a title
* poll reports failure on a working transition. The wx FRAME LIST is the
* signal: `ModEditFrame` appearing IS the Footprint Editor opening.
* - Don't use the `demo.kicad_sch` route. It boots its document from the Y.Doc
* and needs apps/sync (:3055), which this suite's stack does not run; the doc
* stalls at "untitled [Unsaved]" and FACE_PCB never starts. The fileless
* `-/eeschema` route needs no sync, and nothing here needs a loaded schematic —
* the bug is about which kiface preloaded its libraries.
*
* The tree assertion is a DELTA, not an absolute count: the schematic editor
* already renders its own dataviewitem rows (symbol libs), so "> 0" would pass
* even with a totally empty footprint tree. Growth past that baseline is exactly
* "the footprint tree got rows".
*
* Determinism: no blind waits — readiness comes from waitForWxApp, the app's own
* `kicadLibs` observable, and polling for the state each step produces.
*/
const SCOPE = 'default';
const BOOT_TIMEOUT = 180000;
/** Names of the live wx top-level frames. */
function frameNames(page: Page): Promise<string[]> {
return page.evaluate(() =>
(window as unknown as { wxElementRegistry: any }).wxElementRegistry
.findAll({})
.filter((e: any) => /Frame$/.test(e.typeName || ''))
.map((e: any) => e.name as string),
);
}
/** Rendered library-tree rows across whatever frames are up. */
function treeRowCount(page: Page): Promise<number> {
return page.evaluate(
() =>
(window as unknown as { wxElementRegistry: any }).wxElementRegistry.findAllRendered({
elementType: 'dataviewitem',
}).length,
);
}
test.describe('footprint editor reached from eeschema', () => {
test.describe.configure({ timeout: 420000 });
test('opens with a populated library tree', async ({ page }) => {
const aborts: string[] = [];
page.on('console', (m) => {
if (/Aborted\(/i.test(m.text())) aborts.push(m.text());
});
// 1) Boot the SCHEMATIC editor — the face whose preload does not cover footprints.
await page.goto(`/${SCOPE}/projects/demo/-/eeschema`);
await waitForWxApp(page, { timeout: BOOT_TIMEOUT });
await expect
.poll(() => page.title(), { message: 'schematic editor up', timeout: BOOT_TIMEOUT })
.toMatch(/Schematic Editor/i);
// The libs bridge must exist before we cross over, or a miss below would be an
// app-plumbing failure rather than the kiface-preload bug under test.
await page.waitForFunction(
() => !!(window as unknown as { kicadLibs?: unknown }).kicadLibs,
null,
{ timeout: 60000 },
);
expect(await frameNames(page)).toEqual(['SchematicFrame']);
// Baseline: the schematic editor's OWN tree rows (symbol libs) — see header.
// It must be SETTLED before we read it: those rows render asynchronously, and
// a baseline sampled too early (0) would weaken the delta below into "> 0",
// which the schematic's own rows could satisfy on their own. Two consecutive
// equal reads = settled.
let baseline = -1;
await expect
.poll(
async () => {
const n = await treeRowCount(page);
const settled = n === baseline;
baseline = n;
return settled;
},
{ message: 'schematic tree row count settled', timeout: 60000, intervals: [1000] },
)
.toBe(true);
// 2) Cross to the Footprint Editor the way a user does: the toolbar button
// (ACTIONS::showFootprintEditor → Kiway().Player( FRAME_FOOTPRINT_EDITOR )).
expect(
await clickByTooltip(page, 'Create, delete and edit board footprints', {
elementType: 'tool',
}),
'Footprint Editor toolbar button found and clicked',
).toBe(true);
// FACE_PCB starts lazily here — this is the first time the pcbnew kiface runs.
await expect
.poll(() => frameNames(page), {
message: 'Footprint Editor frame (ModEditFrame) opened',
timeout: BOOT_TIMEOUT,
})
.toContain('ModEditFrame');
// 3) THE ASSERTION: its library tree is populated. Before the fix the frame
// opened exactly like this and the tree stayed empty, so the row count
// never moved off the schematic editor's baseline.
await expect
.poll(() => treeRowCount(page), {
message: `footprint library tree rows rendered (baseline was ${baseline})`,
timeout: 120000,
})
.toBeGreaterThan(baseline);
await stableShot(page, 'fpedit-from-eeschema-tree.png');
expect(aborts, 'no WASM abort').toEqual([]);
});
});