From 49aaa697fa699a5b96716ff391cfc3e1141ba128 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 9 Oct 2026 13:46:11 +0200 Subject: [PATCH] :sparkles: Check undo and redo of every step in the composable cases The composable cases tested undo only where a case asked for it, so a change generator with a wrong inverse, or a step that splits into several undo entries, went unnoticed. Both runners now run an undo/redo round trip on every variant, unless the case turns it off with a reason. After each user step that adds an undo group, undo must give back the file before the step and redo the file after it; a user step that adds no group must change nothing. At the end of the variant, undoing back to the last assembly step and redoing everything must give back both states. Files compare through a shared normalizer that drops :modified-at and, on request, :position-data, and keeps :touched and :remote-synced. - The pure runner gets a redo stack and a Redo node; a user operation that pushes a group empties it. Case P undoes and redoes a main edit in both suites. - The frontend interpreter runs the round trip through the real undo and redo events, and checks that each step moves the workspace undo index by one entry. Each variant starts with an empty undo stack. A case can mark a failure that a known bug causes. The mark pins, per phase, the signature of the failure (the shape, page and component attributes or the undo index that differ). It covers no other failure, and it fails the case once the bug stops happening. The check found two bugs, both pinned: - F41: when sync moves a copy child to an earlier position, its undo change puts the child back at the same index, so undoing a reorder in a main leaves the copies reordered (case F). - F43: the reflow after a swap or variant switch loses the step's undo group, so its commits land in undo entries of their own and one undo leaves the step half reverted (cases L and M). --- .../frontend/composable-component-tests.md | 35 ++- .../test_helpers/composable/comp/cases.cljc | 180 +++++++++---- .../test_helpers/composable/comp/nodes.cljc | 30 ++- .../test_helpers/composable/comp/runner.cljc | 209 ++++++++++++--- .../composable/comp/undo_check.cljc | 241 ++++++++++++++++++ .../common/test_helpers/composable/core.cljc | 6 + .../app/common/test_helpers/normalize.cljc | 70 +++++ .../logic/composable_runner_test.cljc | 200 +++++++++++++++ .../logic/composable_sync_test.cljc | 14 +- .../composable_tests/comp/sync_test.cljs | 3 + .../composable_tests/interpreter.cljs | 181 ++++++++++++- 11 files changed, 1056 insertions(+), 113 deletions(-) create mode 100644 common/src/app/common/test_helpers/composable/comp/undo_check.cljc create mode 100644 common/src/app/common/test_helpers/normalize.cljc diff --git a/.serena/memories/frontend/composable-component-tests.md b/.serena/memories/frontend/composable-component-tests.md index d3266eaa0c..8bf0c3d770 100644 --- a/.serena/memories/frontend/composable-component-tests.md +++ b/.serena/memories/frontend/composable-component-tests.md @@ -87,13 +87,15 @@ wrapper deftest in BOTH test namespaces (or only the frontend one if it needs fr Two pure runners (no store, JVM + CLJS): - `core/run-variant`/`run-all`: apply nodes only (no sync, `n/undo` throws). For engine tests. - `comp.runner/run-variant`/`run-all`: the case runner. Every comp node declares its kind via - `n/IComponentOperation` (`op-kind`: `:assembly`, `:user`, `:undo`, `:frontend-only`); the + `n/IComponentOperation` (`op-kind`: `:assembly`, `:user`, `:undo`, `:redo`, `:frontend-only`); the runner and the interpreter's `sync-op?` dispatch on it. A NEW node MUST implement it. User ops record their production changes (`n/record-changes`, nil when nothing changed); the runner throws if a `:user` op records nothing or another kind records changes. After a user op the runner syncs to a fixpoint (`ch/components-changed` + `generate-sync-file-changes`, one pass per cascade level, like the frontend watcher) and pushes op + syncs as one undo group. - `n/undo` reverts the latest group, no re-sync. The runner validates the file (against the + `n/undo` reverts the latest group and moves it to a redo stack; `n/redo` applies it again + (interpreter: `dwu/redo`); neither re-syncs. A user op that pushes a group empties the redo + stack; one that changed nothing keeps it. The runner validates the file (against the situation's files) once settled: after the sync fixpoint and after an undo group; never between cascade passes, where an outer copy still points at the component its main dropped (`:component-id-mismatch`), nor with `{:sync? false}`. Assembly ops (create/instantiate/nest/variant @@ -104,8 +106,35 @@ Two pure runners (no store, JVM + CLJS): frontend events call. Not modelled: the `:modified-at` bump, and the swap/switch event follow-ups (token propagation, WASM text resize, layout update). Tests: `common-tests.logic.composable-runner-test`. +- Undo/redo round trip (`comp/undo_check.cljc`, alias `uc`), on by default in the pure runner + (`:undo-check?`): after each `:user` step that pushed a group, undo must give back the + normalized file before it and redo the file after it (the variant continues from the redone + file); a user step that pushed nothing must change nothing (`:no-undo-entry`); at the end, + undo back to the last assembly step (the baseline; a case's own undo below it moves it) and + redo all. Files compare through `app.common.test-helpers.normalize` (`thn/normalize-file`: + drops file/component `:modified-at`, optionally `:position-data`; shapes as plain maps). + Failures are recorded in the situation with labelled diffs (labels die with the variant, so + they are resolved when recorded) and a `:signature`, the set of what differs + (`uc/signature`: `[:object