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
|
|
@ -1020,6 +1020,120 @@ std::string schCollabGetPos( std::string aId )
|
|||
}
|
||||
|
||||
|
||||
// ── ysync-review repro hooks ─────────────────────────────────────────────────
|
||||
// Local-edit test hooks for the ysync-review repro e2e (docs/features/
|
||||
// ysync-review on the ysync-review branch): each drives a REAL SCH_COMMIT via
|
||||
// CallAfter + COROUTINE (the doApply wrapping), so the SCHEMATIC_LISTENER →
|
||||
// flushDiff emit path runs exactly as for a UI edit. Each returns false when
|
||||
// the uuid doesn't resolve, letting the spec distinguish "hook missed the
|
||||
// item" from "differ missed the edit" (bug 04).
|
||||
|
||||
// Delete an item by uuid via a real SCH_COMMIT.
|
||||
bool schCollabTestRemoveItem( std::string aId )
|
||||
{
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return false;
|
||||
|
||||
SCH_SHEET_PATH path;
|
||||
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||
&path, /*allowNull*/ true );
|
||||
|
||||
if( !item )
|
||||
return false;
|
||||
|
||||
SCH_SCREEN* screen = path.LastScreen();
|
||||
|
||||
fr->CallAfter( [fr, item, screen]() {
|
||||
COROUTINE<int, int> cor( [fr, item, screen]( int ) -> int
|
||||
{
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Remove( item, screen );
|
||||
commit.Push( wxT( "Collab test remove" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rotate an item in place (aDeg snapped to 90° CCW steps) — bug 04: a symbol's
|
||||
// GetPosition() is unchanged by an in-place rotation and its json carries no
|
||||
// orientation, so the rotation is invisible to the scalar differ.
|
||||
bool schCollabTestRotateItem( std::string aId, double aDeg )
|
||||
{
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return false;
|
||||
|
||||
SCH_SHEET_PATH path;
|
||||
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||
&path, /*allowNull*/ true );
|
||||
|
||||
if( !item )
|
||||
return false;
|
||||
|
||||
SCH_SCREEN* screen = path.LastScreen();
|
||||
int steps = ( (int) ( aDeg / 90.0 + ( aDeg >= 0 ? 0.5 : -0.5 ) ) % 4 + 4 ) % 4;
|
||||
|
||||
fr->CallAfter( [fr, item, screen, steps]() {
|
||||
COROUTINE<int, int> cor( [fr, item, screen, steps]( int ) -> int
|
||||
{
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Modify( item, screen );
|
||||
|
||||
for( int i = 0; i < steps; ++i )
|
||||
item->Rotate( item->GetPosition(), /*aRotateCCW*/ true );
|
||||
|
||||
commit.Push( wxT( "Collab test rotate" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set a symbol's Value field text — bug 04: fields live inside the symbol (not
|
||||
// in screen->Items()) and the symbol json carries no field text, so the most
|
||||
// common schematic edit after moving things never syncs.
|
||||
bool schCollabTestSetFieldText( std::string aId, std::string aText )
|
||||
{
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return false;
|
||||
|
||||
SCH_SHEET_PATH path;
|
||||
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||
&path, /*allowNull*/ true );
|
||||
|
||||
if( !item || item->Type() != SCH_SYMBOL_T )
|
||||
return false;
|
||||
|
||||
SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
|
||||
SCH_SCREEN* screen = path.LastScreen();
|
||||
wxString text = wxString::FromUTF8( aText.c_str() );
|
||||
|
||||
fr->CallAfter( [fr, sym, screen, text]() {
|
||||
COROUTINE<int, int> cor( [fr, sym, screen, text]( int ) -> int
|
||||
{
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Modify( sym, screen );
|
||||
sym->SetValueFieldText( text );
|
||||
commit.Push( wxT( "Collab test field text" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Programmatically save the in-memory schematic to a .kicad_sch file, without
|
||||
// driving the Save As dialog — eeschema's analogue of pl_editor's
|
||||
// kicadSaveDrawingSheet. Serializes the root sheet via the same SCH_IO_KICAD_SEXPR
|
||||
|
|
@ -1085,6 +1199,8 @@ void kicadSaveSchematic( std::string path )
|
|||
EMSCRIPTEN_BINDINGS(eeschema) {
|
||||
// Programmatic save of the in-memory schematic (round-trip tests, README §A).
|
||||
function("kicadSaveSchematic", &kicadSaveSchematic);
|
||||
// eeschema-only ysync-review repro hook (name not shared with pcbnew).
|
||||
function("kicadCollabTestSetFieldText", &schCollabTestSetFieldText);
|
||||
|
||||
#ifndef KICAD_MERGED_EMBIND
|
||||
// JS names ALSO registered by pcbnew_embind.cpp — in the merged image these are
|
||||
|
|
@ -1099,6 +1215,9 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
|||
function("kicadCollabSnapshotItems", &schCollabSnapshotItems);
|
||||
function("kicadCollabTestMoveFirst", &schCollabTestMoveFirst);
|
||||
function("kicadCollabGetPos", &schCollabGetPos);
|
||||
// ysync-review repro hooks shared with pcbnew (dispatched when merged).
|
||||
function("kicadCollabTestRemoveItem", &schCollabTestRemoveItem);
|
||||
function("kicadCollabTestRotateItem", &schCollabTestRotateItem);
|
||||
#endif // !KICAD_MERGED_EMBIND
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ std::string pcbCollabSnapshot();
|
|||
std::string pcbCollabSnapshotItems();
|
||||
std::string pcbCollabTestMoveFirst( int aDx, int aDy );
|
||||
std::string pcbCollabGetPos( std::string aId );
|
||||
bool pcbCollabTestRemoveItem( std::string aId );
|
||||
bool pcbCollabTestRotateItem( std::string aId, double aDeg );
|
||||
|
||||
bool schEditorActive();
|
||||
void schCollabApply( std::string aJson );
|
||||
|
|
@ -51,6 +53,8 @@ std::string schCollabSnapshot();
|
|||
std::string schCollabSnapshotItems();
|
||||
std::string schCollabTestMoveFirst( int aDx, int aDy );
|
||||
std::string schCollabGetPos( std::string aId );
|
||||
bool schCollabTestRemoveItem( std::string aId );
|
||||
bool schCollabTestRotateItem( std::string aId, double aDeg );
|
||||
|
||||
|
||||
// Programmatically open a project file in the running editor frame, without UI
|
||||
|
|
@ -118,6 +122,17 @@ static std::string collabGetPos( std::string aId )
|
|||
return pcbEditorActive() ? pcbCollabGetPos( aId ) : schCollabGetPos( aId );
|
||||
}
|
||||
|
||||
static bool collabTestRemoveItem( std::string aId )
|
||||
{
|
||||
return pcbEditorActive() ? pcbCollabTestRemoveItem( aId ) : schCollabTestRemoveItem( aId );
|
||||
}
|
||||
|
||||
static bool collabTestRotateItem( std::string aId, double aDeg )
|
||||
{
|
||||
return pcbEditorActive() ? pcbCollabTestRotateItem( aId, aDeg )
|
||||
: schCollabTestRotateItem( aId, aDeg );
|
||||
}
|
||||
|
||||
|
||||
EMSCRIPTEN_BINDINGS(kicad_editor) {
|
||||
// Programmatic file open (preferred over UI automation from the web app).
|
||||
|
|
@ -131,6 +146,10 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
|
|||
function("kicadCollabSnapshotItems", &collabSnapshotItems);
|
||||
function("kicadCollabTestMoveFirst", &collabTestMoveFirst);
|
||||
function("kicadCollabGetPos", &collabGetPos);
|
||||
// ysync-review repro hooks (shared names; per-editor-only hooks — pad size,
|
||||
// endpoint, field text — flow from the per-editor blocks unchanged).
|
||||
function("kicadCollabTestRemoveItem", &collabTestRemoveItem);
|
||||
function("kicadCollabTestRotateItem", &collabTestRotateItem);
|
||||
}
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <pad.h>
|
||||
#include <pcb_track.h>
|
||||
#include <pcb_field.h>
|
||||
#include <pcb_shape.h>
|
||||
#include <pcb_text.h>
|
||||
#include <pcb_group.h>
|
||||
#include <zone.h>
|
||||
|
|
@ -1149,6 +1150,141 @@ std::string kicadCollabTestItemBlob( std::string aId )
|
|||
return "";
|
||||
}
|
||||
|
||||
// ── ysync-review repro hooks ─────────────────────────────────────────────────
|
||||
// Local-edit test hooks for the ysync-review repro e2e (docs/features/
|
||||
// ysync-review on the ysync-review branch): each drives a REAL BOARD_COMMIT on
|
||||
// the app main stack inside a COROUTINE fiber (the collabTestMove wrapping —
|
||||
// virtual item mutators mis-dispatch off the fiber stack), so the
|
||||
// COLLAB_LISTENER → flushDiff emit path runs exactly as for a UI edit. Each
|
||||
// returns false when the uuid doesn't resolve, letting the spec distinguish
|
||||
// "hook missed the item" from "differ missed the edit" (bug 04).
|
||||
|
||||
// Resolve a live board item by uuid, or null (shared by the hooks below).
|
||||
static BOARD_ITEM* testResolve( PCB_EDIT_FRAME* aFrame, const std::string& aId )
|
||||
{
|
||||
if( !aFrame )
|
||||
return nullptr;
|
||||
|
||||
return aFrame->GetBoard()->ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||
/*allowNullptr*/ true );
|
||||
}
|
||||
|
||||
// Delete an item by uuid. With a footprint CHILD uuid this is the bug-03
|
||||
// sending half (the UI's fp-text delete): the emit must lift to a parent
|
||||
// re-blob; today it goes out as a bare child removal.
|
||||
bool pcbCollabTestRemoveItem( std::string aId )
|
||||
{
|
||||
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||
BOARD_ITEM* item = testResolve( fr, aId );
|
||||
|
||||
if( !item )
|
||||
return false;
|
||||
|
||||
fr->CallAfter( [fr, item]() {
|
||||
COROUTINE<int, int> cor( [fr, item]( int ) -> int
|
||||
{
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Remove( item );
|
||||
commit.Push( wxT( "Collab test remove" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rotate an item about its OWN anchor — bug 04: the scalar json carries no
|
||||
// orientation for footprints, so an anchor-centred rotation is invisible to
|
||||
// the differ unless a child's absolute position happens to move.
|
||||
bool pcbCollabTestRotateItem( std::string aId, double aDeg )
|
||||
{
|
||||
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||
BOARD_ITEM* item = testResolve( fr, aId );
|
||||
|
||||
if( !item )
|
||||
return false;
|
||||
|
||||
fr->CallAfter( [fr, item, aDeg]() {
|
||||
COROUTINE<int, int> cor( [fr, item, aDeg]( int ) -> int
|
||||
{
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Modify( item );
|
||||
item->Rotate( item->GetPosition(),
|
||||
EDA_ANGLE( aDeg, DEGREES_T ) );
|
||||
commit.Push( wxT( "Collab test rotate" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resize a pad (the pad-properties dialog edit) — bug 04: pads are not visited
|
||||
// by forEachTopItem at all, so the edit never reaches either wire.
|
||||
bool pcbCollabTestSetPadSize( std::string aId, int aW, int aH )
|
||||
{
|
||||
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||
BOARD_ITEM* item = testResolve( fr, aId );
|
||||
|
||||
if( !item || item->Type() != PCB_PAD_T )
|
||||
return false;
|
||||
|
||||
PAD* pad = static_cast<PAD*>( item );
|
||||
|
||||
fr->CallAfter( [fr, pad, aW, aH]() {
|
||||
COROUTINE<int, int> cor( [fr, pad, aW, aH]( int ) -> int
|
||||
{
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Modify( pad );
|
||||
pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( aW, aH ) );
|
||||
commit.Push( wxT( "Collab test pad size" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drag a track/shape END point only — bug 04: Drawings' json is position-only
|
||||
// and GetPosition() is the START, so an end-point reshape of a graphic shape
|
||||
// is invisible (tracks DO carry endpoints — the visible control case).
|
||||
bool pcbCollabTestMoveEndpoint( std::string aId, int aDx, int aDy )
|
||||
{
|
||||
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||
BOARD_ITEM* item = testResolve( fr, aId );
|
||||
|
||||
if( !item || ( !isTrackType( item->Type() ) && item->Type() != PCB_SHAPE_T ) )
|
||||
return false;
|
||||
|
||||
fr->CallAfter( [fr, item, aDx, aDy]() {
|
||||
COROUTINE<int, int> cor( [fr, item, aDx, aDy]( int ) -> int
|
||||
{
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Modify( item );
|
||||
|
||||
if( isTrackType( item->Type() ) )
|
||||
{
|
||||
auto* t = static_cast<PCB_TRACK*>( item );
|
||||
t->SetEnd( t->GetEnd() + VECTOR2I( aDx, aDy ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* s = static_cast<PCB_SHAPE*>( item );
|
||||
s->SetEnd( s->GetEnd() + VECTOR2I( aDx, aDy ) );
|
||||
}
|
||||
|
||||
commit.Push( wxT( "Collab test endpoint" ) );
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wrapper to return footprints as vector for JS iteration
|
||||
std::vector<FOOTPRINT*> Board_GetFootprints(BOARD* board) {
|
||||
if (!board) return {};
|
||||
|
|
@ -1218,6 +1354,9 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
|||
function("kicadSaveBoard", &kicadSaveBoard);
|
||||
// pcbnew-only test helper (no eeschema counterpart — name is not shared).
|
||||
function("kicadCollabTestItemBlob", &kicadCollabTestItemBlob);
|
||||
// pcbnew-only ysync-review repro hooks (names not shared with eeschema).
|
||||
function("kicadCollabTestSetPadSize", &pcbCollabTestSetPadSize);
|
||||
function("kicadCollabTestMoveEndpoint", &pcbCollabTestMoveEndpoint);
|
||||
|
||||
#ifndef KICAD_MERGED_EMBIND
|
||||
// JS names ALSO registered by eeschema_embind.cpp — in the merged image these are
|
||||
|
|
@ -1232,6 +1371,9 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
|||
function("kicadCollabSnapshotItems", &pcbCollabSnapshotItems);
|
||||
function("kicadCollabTestMoveFirst", &pcbCollabTestMoveFirst);
|
||||
function("kicadCollabGetPos", &pcbCollabGetPos);
|
||||
// ysync-review repro hooks shared with eeschema (dispatched when merged).
|
||||
function("kicadCollabTestRemoveItem", &pcbCollabTestRemoveItem);
|
||||
function("kicadCollabTestRotateItem", &pcbCollabTestRotateItem);
|
||||
#endif // !KICAD_MERGED_EMBIND
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in a new issue