host/tests: Phase 3 — the engine renders the picker to a CONSOLE (non-browser target)
web/console-render.sx: render-to-console walks a live DOM element tree through the engine's own dom-* accessors and prints it as terminal text — the results <ul> becomes a bulleted list, the filter <input> a text field, the load-more sentinel a "…" line, an .sx-error element a flagged line. It's the console platform's draw step: the browser PAINTS the engine's tree, the harness ASSERTS it, this PRINTS it — one tree, three bindings, the proof the engine is a general runtime not a browser library. Wired into the picker's SX engine tests (web/tests/test-relate-picker.sx): the load and error tests now ALSO assert their console rendering — the same tree the engine built drives both the DOM assertion and the terminal output, so Phase 1's suite is the console renderer's regression suite for free. Plus a relate-picker:console suite for the field/bullet/sentinel/error shapes. 7/7 green, no web-suite regressions. (Class membership reads the live classList via dom-has-class?, not the static class attribute — the engine adds .sx-error through classList.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
94
web/console-render.sx
Normal file
94
web/console-render.sx
Normal file
@@ -0,0 +1,94 @@
|
||||
;; web/console-render.sx — render a live DOM element tree to terminal text.
|
||||
;;
|
||||
;; The SX hypermedia engine builds the SAME element tree whatever the platform: a
|
||||
;; browser PAINTS that tree, the test harness ASSERTS it, and this PRINTS it. Three
|
||||
;; bindings of one tree — the proof that the engine is a general runtime, not a
|
||||
;; browser library (Phase 3 of plans/sx-native-engine-tests.md).
|
||||
;;
|
||||
;; It reads the tree through the engine's own dom-* accessors, so it renders
|
||||
;; whatever the engine produced — e.g. the ~relate-picker after a load/filter/paging
|
||||
;; swap: the results <ul> becomes a bulleted list, the filter <input> a text field,
|
||||
;; the load-more sentinel a "…" line, and an .sx-error element a flagged line. The
|
||||
;; same render-to-console is the console platform's draw step; the picker's SX engine
|
||||
;; tests (web/tests/test-relate-picker.sx) double as its regression suite — they
|
||||
;; already drive the tree, this just prints it.
|
||||
|
||||
;; class membership via the live classList (dom-has-class?), NOT the parsed `class`
|
||||
;; attribute — the engine adds .sx-error through classList, which leaves the static
|
||||
;; attribute untouched, so an attribute read would miss it.
|
||||
(define cr/has-class?
|
||||
(fn (el cls) (dom-has-class? el cls)))
|
||||
|
||||
;; The visible text of an element: its own textContent when it has no element
|
||||
;; children, else the concatenation of its children's text (the mock parser hangs
|
||||
;; leaf text on textContent, so this covers both row buttons and the sentinel).
|
||||
(define cr/text-of
|
||||
(fn (el)
|
||||
(if (nil? el)
|
||||
""
|
||||
(let ((kids (dom-child-list el)))
|
||||
(if (empty? kids)
|
||||
(or (host-get el "textContent") "")
|
||||
(let ((parts (map cr/text-of kids)))
|
||||
(let ((joined (join "" parts)))
|
||||
(if (= joined "")
|
||||
(or (host-get el "textContent") "")
|
||||
joined))))))))
|
||||
|
||||
;; A <ul>/<ol> renders one line per <li>: a bullet for a candidate row, an ellipsis
|
||||
;; for the load-more sentinel (.rp-more).
|
||||
(define cr/list-lines
|
||||
(fn (el)
|
||||
(map
|
||||
(fn (li)
|
||||
(if (cr/has-class? li "rp-more")
|
||||
(str " … " (cr/text-of li))
|
||||
(str " • " (cr/text-of li))))
|
||||
(dom-child-list el))))
|
||||
|
||||
;; An <input> renders as a labelled text field; hidden inputs aren't drawn.
|
||||
(define cr/input-line
|
||||
(fn (el)
|
||||
(let ((type (or (dom-get-attr el "type") "text")))
|
||||
(if (= type "hidden")
|
||||
""
|
||||
(let ((label (or (dom-get-attr el "placeholder") (dom-get-attr el "name") ""))
|
||||
(val (or (host-get el "value") "")))
|
||||
(str label ": [" (if (= val "") " " val) "]"))))))
|
||||
|
||||
;; Flatten the rendered lines of an element's children, dropping blanks.
|
||||
(define cr/children-lines
|
||||
(fn (el)
|
||||
(let ((out (list)))
|
||||
(for-each
|
||||
(fn (kid)
|
||||
(for-each
|
||||
(fn (ln) (when (not (= ln "")) (append! out ln)))
|
||||
(cr/lines kid)))
|
||||
(dom-child-list el))
|
||||
out)))
|
||||
|
||||
;; Render one element to a list of text lines, tag-driven. An .sx-error element is
|
||||
;; flagged with a leading status line (the terminal's red line).
|
||||
(define cr/lines
|
||||
(fn (el)
|
||||
(if (nil? el)
|
||||
(list)
|
||||
(let ((tag (lower (dom-tag-name el)))
|
||||
(err (cr/has-class? el "sx-error")))
|
||||
(let
|
||||
((body
|
||||
(cond
|
||||
(or (= tag "ul") (= tag "ol")) (cr/list-lines el)
|
||||
(= tag "input") (let ((l (cr/input-line el))) (if (= l "") (list) (list l)))
|
||||
(= tag "button") (list (str "[ " (cr/text-of el) " ]"))
|
||||
(or (= tag "h1") (= tag "h2") (= tag "h3")) (list (upper (cr/text-of el)))
|
||||
:else (cr/children-lines el))))
|
||||
(if err
|
||||
(cons "✖ connection problem — retrying…" body)
|
||||
body))))))
|
||||
|
||||
;; render-to-console — the DOM-tree → terminal-text mode, alongside render-to-html /
|
||||
;; render-to-dom. Returns the rendered tree as a single newline-joined string.
|
||||
(define render-to-console
|
||||
(fn (el) (join "\n" (cr/lines el))))
|
||||
@@ -117,7 +117,7 @@
|
||||
" sx-swap=\"innerHTML\""
|
||||
" sx-retry=\"exponential:1000:30000\">"
|
||||
"<input type=\"hidden\" name=\"kind\" value=\"" kind "\">"
|
||||
"<input type=\"text\" name=\"q\" class=\"rp-filter\">"
|
||||
"<input type=\"text\" name=\"q\" class=\"rp-filter\" placeholder=\"filter…\">"
|
||||
"<ul id=\"rp-" kind "-results\" class=\"rp-results\"></ul>"
|
||||
"</form>")))
|
||||
|
||||
@@ -200,6 +200,12 @@
|
||||
(process-elements root)
|
||||
(assert-true (> _mock-fetch-calls 0))
|
||||
(assert-equal 5 (count-candidates results))
|
||||
;; Phase 3: the SAME populated tree renders to the console — the results
|
||||
;; <ul> becomes a bulleted list of candidate titles. (Console renderer driven
|
||||
;; for free by the engine tree; web/console-render.sx.)
|
||||
(let ((txt (render-to-console results)))
|
||||
(assert-true (contains? txt "• Picker Item 0"))
|
||||
(assert-true (contains? txt "• Picker Item 4")))
|
||||
(clear-root! root))))
|
||||
|
||||
(defsuite
|
||||
@@ -272,6 +278,9 @@
|
||||
;; visible failure state: .sx-error lands on the picker form
|
||||
(assert-true (dom-has-class? form "sx-error"))
|
||||
(assert-equal 0 (count-candidates results))
|
||||
;; Phase 3: the console rendering of the errored picker shows the failure as
|
||||
;; a flagged line (the terminal's "red line") — same tree, different binding.
|
||||
(assert-true (contains? (render-to-console form) "✖"))
|
||||
;; recovery: the endpoint works again, the next input retries and the error
|
||||
;; clears as the results populate
|
||||
(set! _mock-fetch-fail false)
|
||||
@@ -280,3 +289,41 @@
|
||||
(assert-false (dom-has-class? form "sx-error"))
|
||||
(assert-equal 3 (count-candidates results))
|
||||
(clear-root! root))))
|
||||
|
||||
;; ── Phase 3: the engine drives a non-browser target (the console) ───
|
||||
;; render-to-console (web/console-render.sx) prints the live engine tree as text.
|
||||
;; These assert the picker's terminal rendering directly on a built tree — the
|
||||
;; console platform's draw step, proven without a terminal.
|
||||
(defsuite
|
||||
"relate-picker:console"
|
||||
(deftest
|
||||
"the picker form renders as a filter field over a bulleted candidate list"
|
||||
(let
|
||||
((root (mk-root (str
|
||||
"<form class=\"relate-picker\">"
|
||||
"<input type=\"hidden\" name=\"kind\" value=\"related\">"
|
||||
"<input type=\"text\" name=\"q\" class=\"rp-filter\" placeholder=\"filter…\">"
|
||||
"<ul class=\"rp-results\">"
|
||||
(row-html "host" "related" "item-0" "Picker Item 0")
|
||||
(row-html "host" "related" "item-1" "Picker Item 1")
|
||||
(sentinel-html "host" "related" 20)
|
||||
"</ul>"
|
||||
"</form>")))
|
||||
(form (dom-query ".relate-picker")))
|
||||
(let ((txt (render-to-console form)))
|
||||
;; the text input becomes a labelled field (placeholder as the label)...
|
||||
(assert-true (contains? txt "filter…: ["))
|
||||
;; ...the hidden kind input is not drawn...
|
||||
(assert-false (contains? txt "related: ["))
|
||||
;; ...each candidate is a bullet, the sentinel an ellipsis line
|
||||
(assert-true (contains? txt "• Picker Item 0"))
|
||||
(assert-true (contains? txt "• Picker Item 1"))
|
||||
(assert-true (contains? txt "… Loading more…")))
|
||||
(clear-root! root)))
|
||||
(deftest
|
||||
"an empty results list renders no bullets"
|
||||
(let
|
||||
((root (mk-root "<ul class=\"rp-results\"></ul>"))
|
||||
(results (dom-query root ".rp-results"))) ;; scope to this root
|
||||
(assert-equal "" (render-to-console results))
|
||||
(clear-root! root))))
|
||||
|
||||
Reference in New Issue
Block a user