fix(ssr): strip UI-only :mode before transaction upsert (500 on advanced manual save)
The :manual save handler builds its tx-data from the wizard snapshot and stripped the control fields :action and :amount-mode, but not :mode (simple/advanced) added by the recent manual-coding work. manual-coding-section* emits step-params[mode] on every render, so EVERY advanced manual save posted :mode "advanced" into :upsert-transaction and 500'd with ":db.error/not-an-entity :mode". Strip :mode alongside :action so the upsert only sees real schema attributes. Also fix the e2e helper that masked this: selectAccountFromTypeahead poked the Alpine v2 internal `el.__x.$data`, which is undefined on Alpine v3 (this app loads alpinejs@3.x), so it silently no-op'd and the account posted empty. Drive the typeahead via the real Alpine v3 path (Alpine.$data + tippy dropdown + click), mirroring transaction-edit-swap. Unmasks the previously-failing "Shared Location spread on save" test (was first in a serial file, hiding 7 siblings). Verified: that test passes; transaction-edit-swap stays 6/6. Skill gotchas.md records the :mode-strip rule, the Alpine-v3 API requirement, and the modal-won't-close diagnosis recipe.
This commit is contained in:
@@ -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 `<form id="wizard-form">` means
|
||||
validation re-render; a `#error {…}` stack means a 500. Then serialize the form right
|
||||
before save (`new FormData(document.querySelector('#wizard-form'))`) to see exactly what
|
||||
posts. This is how the `:mode` 500 and the empty-account bugs above were isolated.
|
||||
|
||||
## Scorecard exceptions (ratchet violations with a reason)
|
||||
|
||||
_None yet._ Append here if a migration must let a metric regress for a documented reason.
|
||||
|
||||
Reference in New Issue
Block a user