diff --git a/.claude/skills/ssr-form-migration/reference/gotchas.md b/.claude/skills/ssr-form-migration/reference/gotchas.md index 56846ae2..e0ccfdee 100644 --- a/.claude/skills/ssr-form-migration/reference/gotchas.md +++ b/.claude/skills/ssr-form-migration/reference/gotchas.md @@ -50,6 +50,39 @@ come from `GET /test-info`. --- +## UI-only control fields must be stripped before a Datomic upsert + +The wizard snapshot/step-params carry UI control fields that are **not** schema +attributes — `:action`, `:amount-mode`, and (added by the simple/advanced work) `:mode`. +The `:manual` save handler stripped `:action`/`:amount-mode` but not `:mode`, so every +*advanced* manual save passed `:mode "advanced"` into `:upsert-transaction` and 500'd with +`:db.error/not-an-entity :mode`. Lesson: when a save derives its tx-data from the form +snapshot, **strip every non-schema control key** before transacting. The session-backed +wizard engine (Phase 6) avoids this class of bug by storing per-step *validated* data +only — UI control fields never enter the combined data. This was a real production bug +surfaced by the e2e gate, not a test artifact. + +## E2E helpers must use the Alpine **v3** API, not the v2 `__x` internal + +The app loads Alpine v3 (`cdn.jsdelivr.net/npm/alpinejs@3.x.x`). The v2 internal +`el.__x.$data` is **gone** — `el.__x` is `undefined`, so any helper that pokes it silently +no-ops. A stale `selectAccountFromTypeahead` did this and left the posted account empty +(account-controlled by `x-model`, so the raw DOM `.value` you set is overwritten from +Alpine's empty state). Drive components the real way instead: `window.Alpine.$data(el)`, +open the tippy dropdown, inject `elements`, click the result — exactly as +`transaction-edit-swap.spec.ts` does. Probe with +`{ hasLegacy__x: !!el.__x, hasAlpineData: !!window.Alpine.$data(el) }`. + +## Diagnosing a "modal won't close after save" + +The edit modal closes on an `hx-trigger: modalclose` from a *successful* save; a +validation failure re-renders the `#wizard-form` (200), and a server exception returns 500 +(caught by `wrap-error`). To find which: capture POST responses in Playwright +(`page.on('response', …)`), read the `edit-submit` body — a `