| Write dvui's shape in jolt, on SDL3, with no shared object 109c7e4 Veronika Winters 9d ago | 1 | (ns glimmer-jvui.tests |
| 2 | "Every check here runs headless: no window, no SDL, no font, no display. |
| 3 | |
| 4 | jvui takes its measurer as a function, so the whole stack — glimmer's |
| 5 | reconciler, this backend's walk, jvui's layout and event routing — can be |
| 6 | driven with a stub that says eight pixels a character. `jolt test`." |
| 7 | (:require [glimmer.ratom :as ra] |
| 8 | [glimmer.core :as gui] |
| 9 | [glimmer-jvui.core :as jv] |
| 10 | [jvui.core :as c])) |
| 11 | |
| 12 | (def ^:private failures (atom 0)) |
| 13 | |
| 14 | (defn- check! [ok? msg] |
| 15 | (when-not ok? |
| 16 | (swap! failures inc) |
| 17 | (println " FAIL:" msg))) |
| 18 | |
| 19 | (defn- ctx [] |
| 20 | (c/context {:size [400 300] |
| 21 | :measure (fn [s _] [(* 8.0 (count s)) 16.0]) |
| 22 | :line-height (fn [_] 16.0)})) |
| 23 | |
| 24 | (defn- walk [n] (cons n (mapcat walk (:children @n)))) |
| 25 | (defn- tagged [root tag] (first (filter #(= tag (:tag @%)) (walk root)))) |
| 26 | (defn- labels [root] |
| 27 | (map #(str (:label (:props @%))) |
| 28 | (filter #(= :label (:tag @%)) (walk root)))) |
| 29 | |
| 30 | (defn- centre |
| 31 | "The middle of the rectangle jvui gave this node on the last walk." |
| 32 | [n] |
| 33 | (let [[x y w h] (:rect @n)] [(+ x (/ w 2.0)) (+ y (/ h 2.0))])) |
| 34 | |
| 35 | (defn- click-at [x y] |
| 36 | [{:kind :motion :x x :y y} |
| 37 | {:kind :mouse-down :x x :y y :button 1 :clicks 1} |
| 38 | {:kind :mouse-up :x x :y y :button 1}]) |
| 39 | |
| 40 | ;; --- the reconciler reaches the backend -------------------------------------- |
| 41 | |
| 42 | (def ^:private clicks (ra/atom 0)) |
| 43 | (def ^:private on? (ra/atom false)) |
| 44 | |
| 45 | (defn- app [] |
| 46 | [:card {} |
| 47 | [:title {:label "Counter"}] |
| 48 | [:label {:label (str "Count: " (ra/deref clicks))}] |
| 49 | [:button {:label "+1" :kind :primary :on-click #(ra/swap! clicks inc)}] |
| 50 | [:checkbox {:label "loud" :checked (ra/deref on?) |
| 51 | :on-change #(ra/reset! on? %)}]]) |
| 52 | |
| 53 | (defn- check-tree! [] |
| 54 | (let [root (jv/root-node)] |
| 55 | (gui/mount root :page [app]) |
| 56 | (check! (some? (tagged root :card)) "the reconciler built a card") |
| 57 | (check! (some? (tagged root :button)) "and a button") |
| 58 | (check! (some #{"Count: 0"} (labels root)) |
| 59 | "and a label that read the ratom"))) |
| 60 | |
| 61 | (defn- check-walk-places-widgets! [] |
| 62 | (let [root (jv/root-node) cx (ctx)] |
| 63 | (gui/mount root :page [app]) |
| 64 | (jv/render-once root cx) |
| 65 | (let [placed (vals (:data @cx)) |
| 66 | card (first (filter #(and (:rect %) (> (first (:min-size % [0 0])) 100)) |
| 67 | placed))] |
| 68 | (check! (some? card) "the walk placed a container with a rectangle") |
| 69 | (check! (every? (fn [[_ _ w h]] (and (>= w 0) (>= h 0))) |
| 70 | (keep :rect placed)) |
| 71 | "and no rectangle came out negative")))) |
| 72 | |
| 73 | ;; --- a click goes all the way round ------------------------------------------ |
| 74 | |
| 75 | (defn- check-click-fires-the-handler! [] |
| 76 | (let [root (jv/root-node) cx (ctx)] |
| 77 | (ra/reset! clicks 0) |
| 78 | (gui/mount root :page [app]) |
| 79 | (jv/render-once root cx) |
| 80 | (jv/render-once root cx) |
| 81 | (let [before (ra/deref clicks) |
| 82 | [bx by] (centre (tagged root :button))] |
| 83 | (jv/render-once root cx (click-at bx by)) |
| 84 | (check! (= (inc before) (ra/deref clicks)) |
| 85 | (str "the click reached :on-click (" before " -> " |
| 86 | (ra/deref clicks) ")")) |
| 87 | (jv/render-once root cx) |
| 88 | (check! (some #{(str "Count: " (ra/deref clicks))} (labels root)) |
| 89 | "and the reactive re-render reached the tree")))) |
| 90 | |
| 91 | (defn- check-press-alone-is-not-a-click! [] |
| 92 | (let [root (jv/root-node) cx (ctx)] |
| 93 | (ra/reset! clicks 0) |
| 94 | (gui/mount root :page [app]) |
| 95 | (jv/render-once root cx) |
| 96 | (jv/render-once root cx) |
| 97 | (let [[bx by] (centre (tagged root :button))] |
| 98 | (jv/render-once root cx [{:kind :motion :x bx :y by} |
| 99 | {:kind :mouse-down :x bx :y by :button 1 :clicks 1}])) |
| 100 | (check! (zero? (ra/deref clicks)) "a press alone must not fire the handler") |
| 101 | (jv/render-once root cx [{:kind :motion :x 390 :y 290} |
| 102 | {:kind :mouse-up :x 390 :y 290 :button 1}]) |
| 103 | (check! (zero? (ra/deref clicks)) "and releasing off it must not either"))) |
| 104 | |
| 105 | ;; --- identity follows the node, not its index -------------------------------- |
| 106 | |
| 107 | (defn- check-reorder-keeps-identity! [] |
| 108 | ;; The reason every node carries a serial as its jvui key. Render a list, |
| 109 | ;; reorder it, and the ids the walk hands out must travel with the nodes. |
| 110 | (let [root (jv/root-node) cx (ctx) |
| 111 | items (ra/atom [:a :b :c]) |
| 112 | list-app (fn [] |
| 113 | (into [:vbox {}] |
| 114 | (for [k (ra/deref items)] |
| 115 | ^{:key k} [:button {:label (name k)}]))) |
| 116 | ids (fn [] (into {} (map (fn [n] [(:label (:props @n)) (:key @n)]) |
| 117 | (filter #(= :button (:tag @%)) (walk root)))))] |
| 118 | (gui/mount root :page [list-app]) |
| 119 | (jv/render-once root cx) |
| 120 | (let [before (ids)] |
| 121 | (ra/reset! items [:c :a :b]) |
| 122 | (jv/render-once root cx) |
| 123 | (let [after (ids)] |
| 124 | (check! (= 3 (count after)) "the list still has three buttons") |
| 125 | (check! (= (get before "a") (get after "a")) |
| 126 | "a keyed node keeps its jvui key across a reorder") |
| 127 | (check! (= (get before "c") (get after "c")) |
| 128 | "including the one that moved to the front"))))) |
| 129 | |
| 130 | ;; --- the vocabulary ---------------------------------------------------------- |
| 131 | |
| 132 | (defn- check-unknown-tag-is-a-container! [] |
| 133 | (let [root (jv/root-node) cx (ctx)] |
| 134 | (gui/mount root :page [(fn [] [:flerb {} [:label {:label "inside"}]])]) |
| 135 | (jv/render-once root cx) |
| 136 | (check! (some #{"inside"} (labels root)) |
| 137 | "an unknown tag shows its contents rather than raising"))) |
| 138 | |
| 139 | (defn- check-entry-round-trips! [] |
| 140 | (let [root (jv/root-node) cx (ctx) |
| 141 | text (ra/atom "")] |
| 142 | (gui/mount root :page |
| 143 | [(fn [] [:entry {:value (ra/deref text) |
| 144 | :on-change #(ra/reset! text %)}])]) |
| 145 | (jv/render-once root cx) |
| 146 | (let [[ex ey] (centre (tagged root :entry))] |
| 147 | (jv/render-once root cx (click-at ex ey))) ; take focus |
| 148 | (jv/render-once root cx [{:kind :text :text "hi"}]) |
| 149 | (check! (= "hi" (ra/deref text)) |
| 150 | (str "typed text reached :on-change: " (pr-str (ra/deref text)))))) |
| 151 | |
| 152 | (defn- check-checkbox-round-trips! [] |
| 153 | (let [root (jv/root-node) cx (ctx)] |
| 154 | (ra/reset! on? false) |
| 155 | (gui/mount root :page [app]) |
| 156 | (jv/render-once root cx) |
| 157 | (jv/render-once root cx) |
| 158 | (let [[bx by] (centre (tagged root :checkbox))] |
| 159 | (jv/render-once root cx (click-at bx by))) |
| 160 | (check! (true? (ra/deref on?)) |
| 161 | (str "the checkbox reached :on-change: " (ra/deref on?))))) |
| 162 | |
| 163 | ;; --- runner ------------------------------------------------------------------ |
| 164 | |
| 165 | (def ^:private checks |
| 166 | [["the reconciler builds a tree" check-tree!] |
| 167 | ["the walk places widgets" check-walk-places-widgets!] |
| 168 | ["a click fires the handler" check-click-fires-the-handler!] |
| 169 | ["a press alone does not" check-press-alone-is-not-a-click!] |
| 170 | ["a reorder keeps identity" check-reorder-keeps-identity!] |
| 171 | ["an unknown tag is a container" check-unknown-tag-is-a-container!] |
| 172 | ["an entry round-trips" check-entry-round-trips!] |
| 173 | ["a checkbox round-trips" check-checkbox-round-trips!]]) |
| 174 | |
| 175 | (defn -main [& _] |
| 176 | (doseq [[name f] checks] |
| 177 | (println "-" name) |
| 178 | (f)) |
| 179 | (if (zero? @failures) |
| 180 | (println "\nall" (count checks) "checks passed") |
| 181 | (do (println "\n" @failures "failed") (System/exit 1)))) |