| Bring vidya in cfd3e36 nandi 19d ago | 1 | (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))))) |