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.
89 lines
4.7 KiB
Markdown
89 lines
4.7 KiB
Markdown
# Gotchas
|
|
|
|
GROWS every migration. One entry per surprise. Also the home for any **written exception**
|
|
to the scorecard ratchet (a metric that regressed for a documented reason).
|
|
|
|
---
|
|
|
|
## Stale `$refs` / `tippy` after a swap
|
|
|
|
A whole-form swap can run an Alpine event handler *before* the component re-initialises,
|
|
so a handler that dereferences `$refs.input.__x_tippy` or calls `tippy.show()` throws.
|
|
**Always null-guard:** `$refs.input?.__x_tippy?.hide()`, `tippy?.show()`. The
|
|
`transaction-edit-swap.spec.ts` `trackErrors()` helper fails the test on any `pageerror`
|
|
or `console.error`, which is exactly how a stale-ref throw surfaces.
|
|
|
|
## Let the server value win — don't preserve Alpine state across a server-driven change
|
|
|
|
When a server change should update a component (e.g. choosing a vendor sets its default
|
|
account), rebuild that section fresh on the swap so the server-provided value lands
|
|
without keying tricks. The bug this prevents: "changing the vendor a *second* time doesn't
|
|
update the account" because preserved Alpine state shadowed the new server value. If you
|
|
*must* preserve a component, key it by value so a change forces re-init:
|
|
`(assoc attrs :key (str id "--" current-value))`.
|
|
|
|
## Focus dies if the typed input is inside its own swapped region
|
|
|
|
The single most important invariant. Amount field → swap a sibling tbody, not the row.
|
|
Memo → swap nothing. If a caret test (`sameNode`) fails, the input is in its own swap
|
|
region — re-target to a sibling/ancestor that excludes it.
|
|
|
|
## Faked cursors breed duplicate render fns
|
|
|
|
A `with-cursor`/`MapCursor` re-root to fake a deep start forces a `*-no-cursor*` twin.
|
|
Removing the fake lets you delete the twin. Don't "fix" a faked cursor in place — top-root
|
|
it and collapse to one render fn. (See `render-functions.md`.)
|
|
|
|
## Edit Clojure with clojure-mcp tools, not the file editor
|
|
|
|
`clojure_edit` / `clojure_edit_replace_sexp`. If a file won't compile: `clj-paren-repair`
|
|
the file, then retry; if still broken, `lein cljfmt check`. Run tests via `clojure-eval` /
|
|
`clj-nrepl-eval -p PORT`, never `lein test` (slow, last resort).
|
|
|
|
## Solr/typeahead in tests
|
|
|
|
Account/vendor search is backed by Solr, unavailable in tests. To drive a typeahead in
|
|
e2e: type under the 3-char threshold, then inject a result into Alpine state
|
|
(`Alpine.$data(el).elements = [{value, label}]`) and click it — the real click handler,
|
|
`tippy.hide()`, Alpine reactivity, and the HTMX swap all run as in production. Entity ids
|
|
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.
|