nandi/jolt-nativepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

backend_test.clj · 197 lines · 8.6 KBClojure Blame HistoryRaw
Bring vidya in cfd3e36 nandi 19d ago1(ns glimmer-vidya.backend-test
2 "The backend against a real node tree, with no window open.
3
4 Node calls do not need a GL surface — only painting does — so the whole
5 reconcile path can be asserted here: what glimmer creates, what it patches,
6 what it reuses across a reactive change, and what it moves rather than
7 rebuilds. The tree is read back through the same ABI the backend writes it
8 with, so these tests fail if either side of the boundary drifts."
9 (:require [clojure.test :refer [deftest is testing]]
10 [glimmer.core :as ui]
11 [glimmer.ratom :as r]
12 [glimmer-vidya.core :as backend]
13 [glimmer-vidya.ffi :as ffi]))
14
15;; --- reading the tree back ---------------------------------------------------
16(defn- children [node]
17 (mapv #(ffi/node-child-at node %) (range (ffi/node-child-count node))))
18
19(defn- shape
20 "A node as plain data: tag, the props asked for, and its children."
21 ([node] (shape node []))
22 ([node keys]
23 {:tag (ffi/node-tag node)
24 ;; An unset prop reads as the empty string, so it is left out rather than
25 ;; recorded as one — the shape says what a node has, not what it lacks.
26 :props (reduce (fn [acc k]
27 (let [v (ffi/node-get-str node (name k))]
28 (if (seq v) (assoc acc k v) acc)))
29 {}
30 keys)
31 :children (mapv #(shape % keys) (children node))}))
32
33(defn- tags [node] (mapv ffi/node-tag (children node)))
34(defn- labels [node] (mapv #(ffi/node-get-str % "label") (children node)))
35
36(defn- fresh-root
37 "A detached node to mount into, so tests do not share the window root."
38 []
39 (ffi/node-new "window"))
40
41;; --- the backend operations --------------------------------------------------
42(deftest create-applies-props-in-their-own-types
43 (let [{:keys [create! apply-props!]} backend/backend
44 node (create! :button {:label "go" :kind :primary :sensitive false})]
45 (is (= "button" (ffi/node-tag node)))
46 (is (= "go" (ffi/node-get-str node "label")))
47 (testing "a keyword prop crosses as its name"
48 (is (= "primary" (ffi/node-get-str node "kind"))))
49 (testing "a boolean crosses as a boolean, not as text"
50 (is (false? (ffi/node-get-bool node "sensitive"))))
51
52 (apply-props! :button node {:label "stop"})
53 (is (= "stop" (ffi/node-get-str node "label")))
54 (testing "a prop the re-render stopped setting is gone, not stale"
55 ;; A prop that is not set reads as its zero value, so `sensitive` is
56 ;; false here whether it was cleared or never written. What makes a
57 ;; cleared node sensitive again is the painter's default, not this read.
58 (is (= "" (ffi/node-get-str node "kind")))
59 (is (= 0.0 (ffi/node-get-num node "value"))))))
60
61(deftest numbers-and-handlers-are-kept-apart
62 (let [{:keys [create!]} backend/backend
63 node (create! :progress {:value 0.25 :on-click (fn [] :ignored)})]
64 (is (= 0.25 (ffi/node-get-num node "value")))
65 (testing "a handler is held on this side; it is not a prop"
66 (is (= "" (ffi/node-get-str node "on-click"))))))
67
68(deftest hbox-and-vbox-carry-their-orientation
69 (let [{:keys [create!]} backend/backend]
70 (is (= "horizontal" (ffi/node-get-str (create! :hbox {}) "orientation")))
71 (is (= "vertical" (ffi/node-get-str (create! :vbox {}) "orientation")))
72 (testing "both are one node kind; the tag only implies the prop"
73 (is (= "box" (ffi/node-tag (create! :hbox {})))))
74 (testing "an explicit orientation wins"
75 (is (= "vertical"
76 (ffi/node-get-str (create! :hbox {:orientation :vertical}) "orientation"))))))
77
78(deftest children-are-appended-removed-replaced-and-reordered
79 (let [{:keys [create! append-child! remove-child! replace-child! reorder-child!]}
80 backend/backend
81 box (create! :vbox {})
82 a (create! :label {:label "a"})
83 b (create! :label {:label "b"})
84 c (create! :label {:label "c"})]
85 (append-child! :box box a)
86 (append-child! :box box b)
87 (is (= ["a" "b"] (labels box)))
88
89 (replace-child! :box box b c)
90 (is (= ["a" "c"] (labels box)))
91 (testing "the replaced widget is freed, not merely unparented"
92 (is (false? (ffi/node-exists? b))))
93
94 (let [d (create! :label {:label "d"})]
95 (append-child! :box box d)
96 (reorder-child! :box box d nil) ; nil sibling = move to the front
97 (is (= ["d" "a" "c"] (labels box)))
98 (reorder-child! :box box d a) ; after a
99 (is (= ["a" "d" "c"] (labels box)))
100 (remove-child! :box box d)
101 (is (= ["a" "c"] (labels box))))))
102
103(deftest removing-a-container-frees-what-was-inside-it
104 (let [{:keys [create! append-child! remove-child!]} backend/backend
105 root (fresh-root)
106 box (create! :vbox {})
107 leaf (create! :label {:label "gone"})]
108 (append-child! :window root box)
109 (append-child! :box box leaf)
110 (remove-child! :window root box)
111 (is (false? (ffi/node-exists? box)))
112 (is (false? (ffi/node-exists? leaf)))))
113
114;; --- the reconciler on top of it ---------------------------------------------
115(defn- greeting [who]
116 [:vbox {:spacing 6}
117 [:title {:label "hello"}]
118 [:label {:label who}]])
119
120(deftest a_component_mounts_as_native_nodes
121 (let [root (fresh-root)]
122 (ui/mount root :window [greeting "world"])
123 (is (= {:tag "window"
124 :props {}
125 :children [{:tag "box" :props {}
126 :children [{:tag "title" :props {:label "hello"} :children []}
127 {:tag "label" :props {:label "world"} :children []}]}]}
128 (shape root [:label])))))
129
130(deftest a_reactive_change_patches_the_same_node
131 (let [root (fresh-root)
132 who (r/atom "world")]
133 (ui/mount root :window [(fn [] [:label {:label @who}])])
134 (let [node (first (children root))]
135 (is (= "world" (ffi/node-get-str node "label")))
136 (reset! who "glimmer")
137 (testing "the widget is patched in place rather than rebuilt"
138 (is (= [node] (children root)))
139 (is (= "glimmer" (ffi/node-get-str node "label")))))))
140
141(deftest a_changed_tag_replaces_the_widget
142 (let [root (fresh-root)
143 loading? (r/atom true)]
144 (ui/mount root :window [(fn [] (if @loading?
145 [:spinner {:label "…"}]
146 [:label {:label "done"}]))])
147 (let [before (first (children root))]
148 (is (= "spinner" (ffi/node-tag before)))
149 (reset! loading? false)
150 (is (= ["label"] (tags root)))
151 (is (false? (ffi/node-exists? before))))))
152
153(deftest a_keyed_list_reuses_widgets_across_a_reorder
154 (let [root (fresh-root)
155 items (r/atom [:a :b :c])]
156 (ui/mount root :window
157 [(fn []
158 (into [:vbox {}]
159 (for [item @items]
160 ^{:key item} [:label {:label (name item)}])))])
161 (let [box (first (children root))
162 before (children box)]
163 (is (= ["a" "b" "c"] (labels box)))
164 (reset! items [:c :a :b])
165 (testing "the same three nodes, in a new order"
166 (is (= ["c" "a" "b"] (labels box)))
167 (is (= (set before) (set (children box)))))
168 (testing "and a dropped key takes its widget with it"
169 (reset! items [:c :b])
170 (is (= ["c" "b"] (labels box)))
171 (is (= 2 (ffi/node-child-count box)))))))
172
173;; --- the hiccup dump ---------------------------------------------------------
174(deftest a_dump_reads_back_as_the_hiccup_that_was_mounted
175 (let [root (fresh-root)]
176 (ui/mount root :window [greeting "world"])
177 (testing "the tree comes back as data, one node per hiccup vector"
178 ;; :vbox is a box with an orientation down there, and :spacing crossed as
179 ;; a double — so this is the mounted hiccup, not a copy of its source.
180 (is (= [:window {}
181 [:box {:orientation "vertical" :spacing 6}
182 [:title {:label "hello"}]
183 [:label {:label "world"}]]]
184 (backend/dump root))))
185 (testing "and as text, for a bug report"
186 (is (= "[:window {}\n [:box {:orientation \"vertical\" :spacing 6}\n [:title {:label \"hello\"}]\n [:label {:label \"world\"}]]]"
187 (backend/dump-str root))))))
188
189(deftest a_dump_shows_a_reactive_change_and_hides_handlers
190 (let [root (fresh-root)
191 who (r/atom "world")]
192 (ui/mount root :window [(fn [] [:button {:label @who :on-click (fn [] nil)}])])
193 (is (= [:window {} [:button {:label "world"}]] (backend/dump root)))
194 (testing "the handler is not in it — it never crossed the boundary"
195 (is (not (contains? (second (nth (backend/dump root) 2)) :on-click))))
196 (reset! who "glimmer")
197 (is (= [:window {} [:button {:label "glimmer"}]] (backend/dump root)))))