Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,19 @@ Three things need to be proven against a fixture before calling this beyond pre-

Does `scripts/eval-cljs.sh '(+ 1 2)'` return `{:ok? true :value 3}`? If not, `ops.clj`'s `cljs-eval-value` parsing needs adjustment.

### 3. `data-rf2-source-coord` format
### 3. `data-rf2-source-coord` format — RESOLVED 2026-05-09 (rf2-7g2q)

`runtime.cljs/parse-rf2-coord` assumes a `'<ns>|<file>:<line>:<col>'` shape. The actual format re-frame2 emits when `(rf/configure :source-coords {:annotate-dom? true})` is on needs verification. Tool-Pair.md §Source-mapping declares the value format opaque — we parse to recover `:ns` / `:line` / `:file`, but the exact separators may differ. Update the parser in one place if so.
Resolved against re-frame2 rf2-z7f7 (PR #135). Per [Spec 006 §Source-coord annotation](https://github.com/day8/re-frame2/blob/master/spec/006-ReactiveSubstrate.md) and [Tool-Pair §Source-mapping](https://github.com/day8/re-frame2/blob/master/spec/Tool-Pair.md) the emitted attribute value is:

```
data-rf2-source-coord="<ns>:<handler-id>:<line>:<col>"
```

Four colon-separated segments, where `<ns>` and `<handler-id>` derive from the registry id keyword (`(namespace id)` / `(name id)`). Either coord segment may be the literal `?` for programmatic `reg-view*` calls that bypassed the macro path. Non-DOM roots (Fragment `:<>`, `:>` interop, fn-component head) are exempt — pair tools fall back to `(rf/handler-meta :view id)` for those.

`scripts/runtime.cljs/parse-rf2-coord` now returns `{:ns :handler-id :line :col}` (or nil for malformed / non-4-segment input). Verified by `tests/runtime/parse_rf2_coord_test.clj` (run via `bb`).

> **Compatibility note.** Tool-Pair.md declares the attribute's value format opaque to consumers — pair2 parses it pragmatically so the DOM-to-source bridge can be useful, but skill consumers MUST NOT depend on the parsed shape's stability across re-frame2 versions. If the format shifts, update the parser (and these tests) in one place.

---

Expand Down
3 changes: 1 addition & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ node out/test.js

Failing points to flag on first run:

- `parse-rf2-coord` assumes a `'<ns>|<file>:<line>:<col>'` shape for `data-rf2-source-coord`. Real attribute format needs verification (Tool-Pair §Source-mapping declares the format opaque).
- ~~`parse-rf2-coord` assumes a `'<ns>|<file>:<line>:<col>'` shape for `data-rf2-source-coord`.~~ Resolved 2026-05-09 (rf2-7g2q): parser updated to the canonical `<ns>:<handler-id>:<line>:<col>` shape per Spec 006 §Source-coord annotation. Bb-runnable tests at `tests/runtime/parse_rf2_coord_test.clj` until the shadow-cljs harness lands.
- `parse-rc-src` assumes a `"file:line"` or `"file:line:column"` format for `data-rc-src`. Real re-com attribute format needs verification.

## 2. Bash-shim integration (`tests/shim/`)
Expand Down Expand Up @@ -84,7 +84,6 @@ Mitigation until we can run E2E per-push:
## What's explicitly **not** tested yet

- Connection against a real shadow-cljs build with nREPL enabled
- `data-rf2-source-coord` format verification
- Multi-frame routing under real concurrency
- `restore-epoch` failure-mode traces
- Hot-reload probe-form selection heuristics
Expand Down
103 changes: 73 additions & 30 deletions scripts/runtime.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -536,30 +536,59 @@
;;
;; Two attribute sources, in priority order:
;; 1. data-rf2-source-coord (re-frame2's own annotation when
;; :annotate-dom? is on — Tool-Pair §Source-mapping)
;; :annotate-dom? is on — Tool-Pair §Source-mapping;
;; Spec 006 §Source-coord annotation, rf2-z7f7)
;; 2. data-rc-src (re-com's debug attribute, fallback)
;;
;; Both parse to {:ns :file :line :column}. The runtime returns the
;; first one it finds.
;; The two attributes resolve to different schemas — re-frame2's
;; carries the registry-id derived <ns>/<handler-id> with <line>/<col>;
;; re-com's carries <file>/<line>/<column>. The runtime returns
;; whichever map the first present attribute parses to.

(defn- parse-rf2-coord
"Parse re-frame2's `data-rf2-source-coord` attribute. Per
Tool-Pair §Source-mapping, the value format is opaque; this
parser uses the documented `'<ns>|<file>:<line>:<col>'` shape used
by the reference implementation. If the actual shape ships
different, update this in one place."
"Parse re-frame2's `data-rf2-source-coord` attribute.

Per Spec 006 §Source-coord annotation (rf2-z7f7) and Tool-Pair
§Source-mapping the attribute value is a four-segment colon-
separated string:

<ns>:<handler-id>:<line>:<col>

where <ns> and <handler-id> derive from the registry id keyword
(`(namespace id)` and `(name id)`) and <line>/<col> are the
captured source coords from the reg-view macro. Either coord
segment may be the literal `?` for programmatic registrations
that bypassed the macro path (Spec 006 §Attribute value format).

Returns
{:ns <string>
:handler-id <string>
:line <int|nil>
:col <int|nil>}

- :line and :col are nil when the corresponding segment is `?`
or otherwise non-numeric.
- Returns nil for blank input, non-strings, or fewer than four
segments. Pair-shaped consumers fall back to (rf/handler-meta
:view id) for those cases (Spec 006 §Documented exemption).

Tool-Pair.md declares the value format opaque to consumers; this
parser exists so pair2's DOM-to-source bridge can be useful, but
downstream callers MUST NOT depend on the parsed shape's
stability across re-frame2 versions."
[attr-val]
(when (and (string? attr-val) (seq attr-val))
(let [[ns-part loc-part] (str/split attr-val #"\|" 2)
parts (when loc-part (str/split loc-part #":"))
valid-int? (fn [s] (and s (re-matches #"\d+" s)))]
(when (and (seq parts) (valid-int? (nth parts 1 nil)))
{:ns ns-part
:file (first parts)
:line (js/parseInt (nth parts 1) 10)
:column (when (and (>= (count parts) 3)
(valid-int? (nth parts 2 nil)))
(js/parseInt (nth parts 2) 10))}))))
(let [parts (str/split attr-val #":")]
(when (= 4 (count parts))
(let [[ns-part sym-part line-part col-part] parts
parse-int (fn [s]
(when (and (string? s) (re-matches #"\d+" s))
(js/parseInt s 10)))]
(when (and (seq ns-part) (seq sym-part))
{:ns ns-part
:handler-id sym-part
:line (parse-int line-part)
:col (parse-int col-part)}))))))

(defn- parse-rc-src
"Parse re-com's `data-rc-src` attribute into {:file :line :column}.
Expand Down Expand Up @@ -629,8 +658,12 @@

(defn dom-source-at
"Given a CSS selector (or `:last-clicked`), return the source coord
{:ns :file :line :column} attached by re-frame2's annotation or
re-com's debug path. Returns a structured result."
attached by re-frame2's annotation or re-com's debug path. Shape
depends on which attribute matched:
- re-frame2: {:ns :handler-id :line :col} (Spec 006 §Source-coord
annotation)
- re-com: {:file :line :column}
Returns a structured result wrapping the coord under :src."
[selector]
(if-let [el (selector-or-last-clicked selector)]
(if-let [coord (read-coord-from-element el)]
Expand All @@ -644,10 +677,18 @@
"Nothing clicked this session; interact with the page first, or pass a CSS selector instead.")}))

(defn dom-find-by-src
"Find live DOM elements whose source-coord attributes mention
file+line. Searches both `data-rf2-source-coord` and `data-rc-src`."
[file line]
(let [needle (str file ":" line)
"Find live DOM elements whose source-coord attributes mention the
needle `<file-or-id>:<line>`. Searches both `data-rf2-source-coord`
and `data-rc-src`.

For `data-rc-src` the needle pairs the source file and line.
For `data-rf2-source-coord` (whose value is `<ns>:<handler-id>:<line>:<col>`,
per Spec 006 §Source-coord annotation) callers typically pass the
handler-id as the first argument so the substring match hits the
handler-id segment. Pair-shaped consumers wanting <ns>-scoped
queries should construct their own selector."
[file-or-id line]
(let [needle (str file-or-id ":" line)
nodes (.querySelectorAll js/document
(str "[data-rf2-source-coord*='" needle "'],"
"[data-rc-src*='" needle "']"))]
Expand All @@ -659,11 +700,13 @@
:src (read-coord-from-element node)})))))

(defn dom-fire-click
"Synthesise a click on the element matching file+line. Picks the
first match if multiple. Returns {:ok? true :clicked {...}} — the
resulting epoch lands asynchronously, fetch it with `last-epoch`."
[file line]
(let [needle (str file ":" line)
"Synthesise a click on the element whose source-coord attribute
contains the substring `<file-or-id>:<line>`. Picks the first
match if multiple. Returns {:ok? true :clicked {...}} — the
resulting epoch lands asynchronously, fetch it with `last-epoch`.
See `dom-find-by-src` for the needle semantics."
[file-or-id line]
(let [needle (str file-or-id ":" line)
selector (str "[data-rf2-source-coord*='" needle "'],"
"[data-rc-src*='" needle "']")
el (.querySelector js/document selector)]
Expand All @@ -673,7 +716,7 @@
{:ok? true
:clicked {:tag (.toLowerCase (.-tagName el))
:id (not-empty (.-id el))}})
{:ok? false :reason :no-element-at-src :file file :line line})))
{:ok? false :reason :no-element-at-src :file-or-id file-or-id :line line})))

(defn dom-describe
"Summarise a DOM element."
Expand Down
210 changes: 210 additions & 0 deletions tests/runtime/parse_rf2_coord_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
;;;; tests/runtime/parse_rf2_coord_test.clj
;;;;
;;;; Babashka-runnable verification of `parse-rf2-coord` from
;;;; `scripts/runtime.cljs`.
;;;;
;;;; Why a parallel implementation lives here:
;;;;
;;;; `scripts/runtime.cljs` is a CLJS-only file injected into the
;;;; browser app over nREPL. It uses `js/parseInt`, so it can't run
;;;; under bb directly. The real shadow-cljs test build (planned per
;;;; `docs/TESTING.md` §1) will exercise the `.cljs` source in place
;;;; under Node — that's the canonical home for these tests once the
;;;; build is wired up.
;;;;
;;;; Until then, this file mirrors the parser logic and asserts
;;;; behaviour against samples cribbed from re-frame2's own tests
;;;; (`implementation/reagent/test/re_frame/source_coord_dom_cljs_test.cljs`
;;;; and `implementation/core/test/re_frame/ssr_source_coord_test.clj`)
;;;; so format drift in `runtime.cljs` shows up under `bb` until the
;;;; shadow-cljs harness lands.
;;;;
;;;; Run: bb tests/runtime/parse_rf2_coord_test.clj
;;;; Exit: 0 = pass, non-zero = fail.

(ns parse-rf2-coord-test
(:require [clojure.string :as str]
[clojure.test :refer [deftest is run-tests testing]]))

;; ---------------------------------------------------------------------------
;; Mirror of scripts/runtime.cljs `parse-rf2-coord`.
;;
;; KEEP IN SYNC WITH scripts/runtime.cljs. Logic is identical except
;; for `js/parseInt` -> `Long/parseLong` (bb runtime). The regex,
;; segment count, and shape are the contract under test.
;; ---------------------------------------------------------------------------

(defn- parse-int-str [s]
(when (and (string? s) (re-matches #"\d+" s))
(Long/parseLong s)))

(defn parse-rf2-coord
"See scripts/runtime.cljs for the canonical version.
Returns {:ns :handler-id :line :col} or nil."
[attr-val]
(when (and (string? attr-val) (seq attr-val))
(let [parts (str/split attr-val #":")]
(when (= 4 (count parts))
(let [[ns-part sym-part line-part col-part] parts]
(when (and (seq ns-part) (seq sym-part))
{:ns ns-part
:handler-id sym-part
:line (parse-int-str line-part)
:col (parse-int-str col-part)}))))))

;; ---------------------------------------------------------------------------
;; Canonical case — the DOM snippet from PR #135 (rf2-z7f7).
;; ---------------------------------------------------------------------------

(deftest canonical-form-1
(testing "canonical 4-segment shape from PR #135"
(is (= {:ns "counter.core"
:handler-id "counter-buttons"
:line 47
:col 11}
(parse-rf2-coord "counter.core:counter-buttons:47:11")))))

;; ---------------------------------------------------------------------------
;; Real Form-1 sample shape — derived from
;; re-frame2/implementation/reagent/test/re_frame/source_coord_dom_cljs_test.cljs
;; (annotates-dom-root-without-attrs). The real test asserts the regex
;; `^rf\.src-coord-test:no-attrs:\d+:\d+$` — concrete sample below.
;; ---------------------------------------------------------------------------

(deftest form-1-rf2-test-sample
(testing "Form-1 sample matching the rf2 test regex"
(is (= {:ns "rf.src-coord-test"
:handler-id "no-attrs"
:line 54
:col 7}
(parse-rf2-coord "rf.src-coord-test:no-attrs:54:7")))))

;; ---------------------------------------------------------------------------
;; Form-2 sample shape — re-frame2 tests confirm the inner output is
;; annotated using the SAME 4-segment format (the wrapper recurses on
;; the inner fn's hiccup; the attribute value is identical to Form-1).
;; Sample: rf.src-coord-test/form-2 from source_coord_dom_cljs_test.cljs.
;; ---------------------------------------------------------------------------

(deftest form-2-inner-output
(testing "Form-2 wrapper annotates inner hiccup with the same 4-segment shape"
(is (= {:ns "rf.src-coord-test"
:handler-id "form-2"
:line 104
:col 5}
(parse-rf2-coord "rf.src-coord-test:form-2:104:5")))))

;; ---------------------------------------------------------------------------
;; SSR sample — derived from
;; re-frame2/implementation/core/test/re_frame/ssr_source_coord_test.clj
;; (reg-view-root-is-annotated). Test asserts the regex
;; `data-rf2-source-coord="rf\.ssr-coord-test:banner:\d+:\d+"`.
;; ---------------------------------------------------------------------------

(deftest ssr-sample
(testing "SSR-rendered registered view carries the same 4-segment format"
(is (= {:ns "rf.ssr-coord-test"
:handler-id "banner"
:line 48
:col 13}
(parse-rf2-coord "rf.ssr-coord-test:banner:48:13")))))

;; ---------------------------------------------------------------------------
;; Programmatic-registration degradation — Spec 006 §Attribute value
;; format: <line>/<col> may be `?` when the macro path was bypassed.
;; The CLJS test (programmatic-registration-degrades-gracefully)
;; asserts the literal value "rf.src-coord-test:programmatic:?:?".
;; ---------------------------------------------------------------------------

(deftest degraded-line-col
(testing "programmatic registration with `?` placeholders parses the id portion;
line/col are nil"
(is (= {:ns "rf.src-coord-test"
:handler-id "programmatic"
:line nil
:col nil}
(parse-rf2-coord "rf.src-coord-test:programmatic:?:?"))))

(testing "partial degradation — only column missing"
(is (= {:ns "ns.x"
:handler-id "view"
:line 42
:col nil}
(parse-rf2-coord "ns.x:view:42:?")))))

;; ---------------------------------------------------------------------------
;; Malformed / edge cases — must never throw; return nil.
;; ---------------------------------------------------------------------------

(deftest malformed-too-few-segments
(testing "too few segments returns nil"
(is (nil? (parse-rf2-coord "ns:view:42")))
(is (nil? (parse-rf2-coord "ns:view")))
(is (nil? (parse-rf2-coord "ns")))
(is (nil? (parse-rf2-coord "")))))

(deftest malformed-too-many-segments
(testing "more than 4 segments returns nil — strict 4-segment shape"
(is (nil? (parse-rf2-coord "a:b:c:d:e")))
(is (nil? (parse-rf2-coord "a:b:1:2:3")))))

(deftest empty-segments
(testing "empty <ns> or <handler-id> segments return nil"
(is (nil? (parse-rf2-coord ":handler:1:2")))
(is (nil? (parse-rf2-coord "ns::1:2")))))

(deftest non-string-input
(testing "non-string input returns nil — never throws"
(is (nil? (parse-rf2-coord nil)))
(is (nil? (parse-rf2-coord 42)))
(is (nil? (parse-rf2-coord :keyword)))
(is (nil? (parse-rf2-coord ["v" "e" "c"])))))

;; ---------------------------------------------------------------------------
;; Hyphenated / dotted ids — re-frame's namespace and registry-id
;; conventions allow dots in the namespace and hyphens in the symbol.
;; ---------------------------------------------------------------------------

(deftest dotted-and-hyphenated
(testing "dotted ns and hyphenated handler-id parse cleanly"
(is (= {:ns "my-app.cart.view"
:handler-id "apply-coupon-button"
:line 125
:col 4}
(parse-rf2-coord "my-app.cart.view:apply-coupon-button:125:4")))))

;; ---------------------------------------------------------------------------
;; Round-trip (idempotence) — re-formatting a parsed coord and parsing
;; that string back yields equivalent shape (line/col only meaningful
;; when both were captured numerically).
;; ---------------------------------------------------------------------------

(defn- format-coord
"Mirror of re-frame2/views.cljs/format-source-coord: emits the
<ns>:<sym>:<line>:<col> shape, with `?` for nil line/col."
[{:keys [ns handler-id line col]}]
(str ns ":" handler-id ":"
(if line (str line) "?")
":"
(if col (str col) "?")))

(deftest round-trip-numeric
(testing "parse -> format -> parse is idempotent for numeric coords"
(let [s "counter.core:counter-buttons:47:11"
once (parse-rf2-coord s)
twice (parse-rf2-coord (format-coord once))]
(is (= once twice)))))

(deftest round-trip-degraded
(testing "parse -> format -> parse is idempotent for ?:? coords"
(let [s "rf.x:programmatic:?:?"
once (parse-rf2-coord s)
twice (parse-rf2-coord (format-coord once))]
(is (= once twice)))))

;; ---------------------------------------------------------------------------
;; Run.
;; ---------------------------------------------------------------------------

(let [{:keys [fail error]} (run-tests 'parse-rf2-coord-test)]
(System/exit (if (and (zero? fail) (zero? error)) 0 1)))
Loading