├── .github └── FUNDING.yml ├── dev.cljs.edn ├── src └── cljs_test_display │ ├── core.clj │ ├── notify.cljs │ ├── favicon.cljs │ └── core.cljs ├── .gitignore ├── dev └── cljs_test_display │ └── dev.cljs ├── deps.edn ├── dev-resources └── public │ └── index.html ├── project.clj ├── test └── cljs_testing │ ├── other_test.cljs │ └── core_test.cljs ├── resources └── public │ └── com │ └── bhauman │ └── cljs-test-display │ └── css │ └── style.css └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [bhauman] 3 | -------------------------------------------------------------------------------- /dev.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:css-dirs ["resources/public/com/bhauman/cljs-test-display"] 2 | :watch-dirs ["src" "dev" "test"]} 3 | {:main cljs-test-display.dev} 4 | -------------------------------------------------------------------------------- /src/cljs_test_display/core.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-test-display.core 2 | (:require 3 | [clojure.java.io :as io])) 4 | 5 | (defmacro css [] 6 | (slurp (io/resource "public/com/bhauman/cljs-test-display/css/style.css"))) 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /out/ 6 | /target/ 7 | .lein-deps-sum 8 | .lein-repl-history 9 | .lein-plugins/ 10 | .repl 11 | .nrepl-port 12 | .cpcache 13 | .rebel_readline_history 14 | pom.xml.asc 15 | -------------------------------------------------------------------------------- /dev/cljs_test_display/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-hooks cljs-test-display.dev 2 | (:require 3 | [cljs-test-display.core :as cljs-display] 4 | [cljs-testing.core-test] 5 | [cljs-testing.other-test])) 6 | 7 | (defn ^:after-load run [] 8 | (cljs.test/run-tests (cljs-display/init! :apper) 9 | 'cljs-testing.core-test 10 | 'cljs-testing.other-test)) 11 | 12 | (defonce runit (run)) 13 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | ;; for development only, don't use this via a git dependency 2 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"} 3 | org.clojure/clojurescript {:mvn/version "1.10.238"} 4 | com.bhauman/rebel-readline-cljs {:mvn/version "0.1.3"} 5 | com.bhauman/figwheel-main {:mvn/version "0.1.3-SNAPSHOT"}} 6 | :paths ["resources" "target" "test" "dev-resources" "src"] 7 | :aliases {:build {:main-opts ["-m" "figwheel.main" "-b" "dev" "-r"]} 8 | :min {:main-opts ["-m" "figwheel.main" "-O" "advanced" "-bo" "dev"]}}} 9 | -------------------------------------------------------------------------------- /dev-resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.bhauman/cljs-test-display "0.1.2-SNAPSHOT" 2 | :description "Provides a visual display for ClojureScript tests." 3 | :url "https://github.com/bhauman/cljs-test-display" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :min-lein-version "2.7.1" 8 | 9 | :dependencies [[org.clojure/clojure "1.9.0"] 10 | [org.clojure/clojurescript "1.10.238" :scope "provided"]] 11 | 12 | :source-paths ["src"] 13 | 14 | :aliases {"fig" ["trampoline" "run" "-m" "figwheel.main"] 15 | "fig:build" ["trampoline" "run" "-m" "figwheel.main" "-b" "dev" "-r"] 16 | "fig:min" ["run" "-m" "figwheel.main" "-O" "advanced" "-bo" "dev"]} 17 | 18 | :profiles {:dev {:dependencies [[com.bhauman/figwheel-main "0.1.2"] 19 | [com.bhauman/rebel-readline-cljs "0.1.3"] 20 | [org.clojure/clojurescript "1.10.238"]] 21 | :source-paths ["src" "dev" "test"] 22 | :resource-paths ["resources" "dev-resources" "target"] 23 | ;; need to add the compliled assets to the :clean-targets 24 | :clean-targets ^{:protect false} ["target/public" :target-path]}}) 25 | 26 | -------------------------------------------------------------------------------- /src/cljs_test_display/notify.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-test-display.notify 2 | (:require 3 | [cljs-test-display.favicon :as favicon] 4 | [goog.object :as gobj])) 5 | 6 | (def notification (gobj/get goog.global "Notification")) 7 | 8 | (defn with-permission [perm thunk] 9 | (when notification 10 | (when (= perm (gobj/get notification "permission")) 11 | (thunk)))) 12 | 13 | (defn ask-permission! [] 14 | (with-permission "default" 15 | #(.requestPermission notification))) 16 | 17 | (def red-url (favicon/color-data-url "#d00" 512)) 18 | (def green-url (favicon/color-data-url "#0d0" 512)) 19 | 20 | (defn success [] 21 | (with-permission "granted" 22 | #(js/Notification. "All CLJS Tests Passed" 23 | #js {:icon green-url :silent true}))) 24 | 25 | (defn failure [{:keys [error fail]}] 26 | (with-permission "granted" 27 | #(js/Notification. "CLJS Tests Failed" 28 | #js {:icon red-url 29 | :silent true 30 | :body (str 31 | (when fail 32 | (str fail " failures ")) 33 | (when error 34 | (str error " errors")))}))) 35 | 36 | #_(success) 37 | #_(failure {}) 38 | -------------------------------------------------------------------------------- /src/cljs_test_display/favicon.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-test-display.favicon 2 | (:require 3 | [clojure.string :as string] 4 | [goog.object :as gobj] 5 | [goog.dom :as gdom])) 6 | 7 | (goog-define link-id "cljs-test-favicon") 8 | 9 | (defn html-collection->seq [html-coll] 10 | (map #(.item html-coll %) (range (.-length html-coll)))) 11 | 12 | (defn find-existing-link [] 13 | (first 14 | (filter 15 | (fn [l] 16 | (when-let [rel (.-rel l)] 17 | (some #(= "icon" %) (string/split rel #"\s")))) 18 | (html-collection->seq (gdom/getElementsByTagName "link"))))) 19 | 20 | (defn init-link! [l] 21 | (set! (.-id l) link-id) 22 | (set! (.-rel l) "shortcut icon") 23 | (set! (.-type l) "image/png") 24 | (set! (.-href l) "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAIElEQVQ4T2NMS0v7z0ABYBw1gGE0DBhGwwCYh4ZBOgAAcQUjIUXh8RYAAAAASUVORK5CYII=")) 25 | 26 | (defn get-or-create! [] 27 | (if-let [favicon (gdom/getElement link-id)] 28 | favicon 29 | (if-let [favicon (find-existing-link)] 30 | (do (init-link! favicon) 31 | favicon) 32 | (let [favicon (gdom/createDom "link")] 33 | (init-link! favicon) 34 | (-> (gdom/getDocument) 35 | (gobj/get "head") 36 | (gdom/appendChild favicon)) 37 | favicon)))) 38 | 39 | (defn color-data-url [color size] 40 | (let [cvs (gdom/createDom "canvas" #js {:width size :height size})] 41 | (let [ctx (.getContext cvs "2d")] 42 | (set! (.-fillStyle ctx) color) 43 | (.fillRect ctx 0 0 size size)) 44 | (.toDataURL cvs))) 45 | 46 | (defn change-to-color [color] 47 | (set! (.-href (get-or-create!)) (color-data-url color 16))) 48 | 49 | (defn green [] (change-to-color "#0d0")) 50 | 51 | (defn red [] (change-to-color "#d00")) 52 | -------------------------------------------------------------------------------- /test/cljs_testing/other_test.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-testing.other-test 2 | (:require 3 | [cljs.test :refer [deftest is testing]])) 4 | 5 | (deftest test-a 6 | (testing "a should be like b" 7 | (is (= 45 (reduce + (range 10))) "This should work") 8 | (is (= 45 (reduce + (range 10)))) 9 | (is (= 45 (reduce + (range 10)))) 10 | (is (= 45 (reduce + (range 10)))) 11 | (is (= 45 (reduce + (range 10)))) 12 | (is (= 45 (reduce + (range 10)))) 13 | ) 14 | 15 | 16 | (testing "a should be like b" 17 | (is (= 45 (reduce + (range 10)))) 18 | (is (= 45 (reduce + (range 10)))) 19 | (is (= 45 (reduce + (range 10)))) 20 | (is (= 45 (reduce + (range 10)))) 21 | (is (= 45 (reduce + (range 10)))) 22 | (is (= 45 (reduce + (range 10)))) 23 | ) 24 | ) 25 | 26 | (deftest test-b 27 | (testing "a should be like b" 28 | (is (= 45 (reduce + (range 10)))) 29 | (is (= 45 (reduce + (range 10)))) 30 | (is (= 45 (reduce + (range 10)))) 31 | (is (= 45 (reduce + (range 10)))) 32 | (is (= 45 (reduce + (range 10)))) 33 | (is (= 45 (reduce + (range 10)))) 34 | ) 35 | 36 | (testing "a should be like b" 37 | (is (= 45 (reduce + (range 10)))) 38 | (is (= 45 (reduce + (range 10)))) 39 | (is (= 45 (reduce + (range 10)))) 40 | (is (= 45 (reduce + (range 10)))) 41 | (is (= 45 (reduce + (range 10)))) 42 | (is (= 45 (reduce + (range 10)))) 43 | ) 44 | ) 45 | 46 | (deftest test-c 47 | (testing "a should be like b" 48 | (is (= 45 (reduce + (range 10)))) 49 | (is (= 45 (reduce + (range 10)))) 50 | (is (= 45 (reduce + (range 10)))) 51 | (is (= 45 (reduce + (range 10)))) 52 | (is (= 45 (reduce + (range 10)))) 53 | (is (= 45 (reduce + (range 10)))) 54 | ) 55 | 56 | (testing "a should be like b" 57 | (is (= 45 (reduce + (range 10)))) 58 | (is (= 45 (reduce + (range 10)))) 59 | (is (= 45 (reduce + (range 10)))) 60 | (is (= 45 (reduce + (range 10)))) 61 | (is (= 45 (reduce + (range 10)))) 62 | (is (= 45 (reduce + (range 10)))) 63 | ) 64 | ) 65 | -------------------------------------------------------------------------------- /test/cljs_testing/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-testing.core-test 2 | (:require 3 | [cljs.test :refer [deftest is testing]])) 4 | 5 | (defn this-throws-an-error [msg] 6 | (throw (js/Error. msg))) 7 | 8 | (deftest test-a 9 | (testing "a should be like b" 10 | (is (= 45 (reduce + (range 10))) "This should work") 11 | (is (= 45 (reduce + (range 10)))) 12 | (is (= 45 (reduce + (range 10)))) 13 | (is (= 45 (reduce + (range 10)))) 14 | (is (= 45 (reduce + (range 10)))) 15 | (is (= 45 (reduce + (range 10)))) 16 | ) 17 | 18 | (testing "a should be like b" 19 | (is (= 45 (reduce + (range 10)))) 20 | (is (= 45 (reduce + (range 10)))) 21 | (is (= 45 (reduce + (range 10)))) 22 | (is (= 45 (reduce + (range 10)))) 23 | (is (= 45 (reduce + (range 10)))) 24 | (is (= 45 (reduce + (range 10)))) 25 | (is (= 45 (reduce + (range 10)))) 26 | (is (= 45 (reduce + (range 10)))) 27 | (is (= 45 (reduce + (range 10)))) 28 | (is (= 45 (reduce + (range 10)))) 29 | (is (= 45 (reduce + (range 10)))) 30 | (is (= 45 (reduce + (range 10)))) 31 | (is (= 45 (reduce + (range 10)))) 32 | (is (= 45 (reduce + (range 10)))) 33 | (is (= 45 (reduce + (range 10)))) 34 | (is (= 45 (reduce + (range 10)))) 35 | (is (= 45 (reduce + (range 10)))) 36 | (is (= 45 (reduce + (range 10)))) 37 | (is (= 45 (reduce + (range 10)))) 38 | (is (= 45 (reduce + (range 10)))) 39 | (is (= 45 (reduce + (range 10)))) 40 | (is (= 45 (reduce + (range 10)))) 41 | (is (= 45 (reduce + (range 10)))) 42 | (is (= 45 (reduce + (range 10)))) 43 | ) 44 | 45 | 46 | ) 47 | 48 | (deftest test-b 49 | (testing "a mouse" 50 | (testing "a should be like b" 51 | (is (= 45 (reduce + (range 10)))) 52 | (is (= 45 (reduce + (range 10)))) 53 | (is (this-throws-an-error "ouch")) 54 | (is (= 45 (reduce + (range 10)))) 55 | (is (= 45 (reduce + (range 10)))) 56 | (is (= 45 (reduce + (range 10)))) 57 | (is (= 45 (reduce + (range 10)))) 58 | ) 59 | 60 | (testing "a should be like b" 61 | (is (= 45 (reduce + (range 10)))) 62 | (is (= 5 (reduce + (range 10))) "it should be equal to 45") 63 | (is (= 5 (reduce + (range 10)))) 64 | (is (= 45 (reduce + (range 10)))) 65 | (is (= 45 (reduce + (range 10)))) 66 | (is (= 45 (reduce + (range 10)))) 67 | )) 68 | ) 69 | 70 | (deftest test-c 71 | (testing "a should be like b" 72 | (is (= 45 (reduce + (range 10)))) 73 | (is (= 45 (reduce + (range 10)))) 74 | (is (= 45 (reduce + (range 10)))) 75 | (is (= 45 (reduce + (range 10)))) 76 | (is (= 45 (reduce + (range 10)))) 77 | (is (= 45 (reduce + (range 10)))) 78 | ) 79 | 80 | (testing "a should be like b" 81 | (is (= 45 (reduce + (range 10)))) 82 | (is (= 45 (reduce + (range 10)))) 83 | (is (= 45 (reduce + (range 10)))) 84 | (is (= 45 (reduce + (range 10)))) 85 | (is (= 45 (reduce + (range 10)))) 86 | (is (= 45 (reduce + (range 10)))) 87 | ) 88 | ) 89 | -------------------------------------------------------------------------------- /resources/public/com/bhauman/cljs-test-display/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | } 4 | 5 | .container { 6 | font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif!important; 7 | font-size: 16px; 8 | line-height: 1.4285; 9 | margin: 0px auto; 10 | padding: 0px 10px; 11 | } 12 | 13 | /* report header */ 14 | 15 | #report-header { 16 | background-color: #27437b; 17 | color: white; 18 | } 19 | 20 | #report-header .report-body { 21 | padding: 20px 10px; 22 | /* hack */ 23 | padding-right: 0px; 24 | } 25 | 26 | #report-header.tests-fail { 27 | background-color: #7b2727; 28 | } 29 | 30 | #report-header.tests-succeed { 31 | background-color: #277b2d; 32 | } 33 | 34 | #report-header .page-title { 35 | display: flex; 36 | align-items: center; 37 | margin-bottom: 1em; 38 | } 39 | 40 | #report-header .test-title { 41 | font-size: 40px; 42 | margin-left: 15px; 43 | } 44 | 45 | /* controls */ 46 | 47 | .controls { 48 | margin-top: 10px; 49 | height: 10px; 50 | } 51 | 52 | .controls button { 53 | float: right; 54 | border-radius: 5px; 55 | font-family: Arial; 56 | color: #ffffff; 57 | font-size: 11px; 58 | background-color: #8e8e8e; 59 | padding: 5px 10px 5px 10px; 60 | text-decoration: none; 61 | border: none; 62 | letter-spacing: 0.8px; 63 | } 64 | 65 | .controls button:hover { 66 | background: #a9a9a9; 67 | text-decoration: none; 68 | } 69 | /* summary */ 70 | 71 | .summary-body { 72 | display: flex; 73 | flex-wrap: wrap; 74 | justify-content: space-between; 75 | align-items: center; 76 | } 77 | 78 | .summary-body .report-number { 79 | margin-right: 20px; 80 | font-family: monospace; 81 | font-size: 2em; 82 | } 83 | 84 | .summary-body .total-tests { 85 | font-size: 0.75em; 86 | line-height: 1.2em; 87 | } 88 | 89 | .summary-body .test-counts { 90 | display: flex; 91 | flex-wrap: wrap; 92 | } 93 | 94 | .summary-body .test-counts > div { 95 | margin-right: 10px; 96 | } 97 | 98 | /* namespace */ 99 | 100 | .test-ns h2 { 101 | font-weight: normal; 102 | color: #333; 103 | } 104 | 105 | /* var line */ 106 | 107 | .var-header { 108 | color: #666; 109 | margin: 8px 0px; 110 | border-bottom: 1px solid #e0e0e0; 111 | 112 | display: flex; 113 | justify-content: space-between; 114 | align-items: baseline; 115 | } 116 | 117 | .test-var-line { 118 | color: #999; 119 | font-size: 0.7em; 120 | } 121 | 122 | .test-var-line span { 123 | font-family: monospace; 124 | } 125 | 126 | /* passing test dot */ 127 | 128 | .test-passing { 129 | display: inline-block; 130 | width: 12px; 131 | height: 12px; 132 | border: 1px solid #98bd8b; 133 | background-color: rgb(199, 225, 160); 134 | } 135 | 136 | .test-passing + .test-passing { 137 | border-left: none; 138 | } 139 | 140 | /* ensure vertical space between passing tests and failing tests */ 141 | 142 | .test-passing + .test-fail { 143 | margin-top: 8px; 144 | } 145 | 146 | .test-fail + .test-passing { 147 | margin-top: 14px; 148 | } 149 | 150 | /* failed tests */ 151 | 152 | .test-fail { 153 | color: #a94442; 154 | border: 1px solid rgb(236, 196, 196); 155 | border-left: 8px solid rgb(236, 196, 196); 156 | background-color: rgb(254, 254, 244); 157 | } 158 | 159 | .fail-body { 160 | padding: 10px 24px; 161 | } 162 | 163 | .contexts { 164 | padding: 0px 5px; 165 | font-size: 0.8em; 166 | background-color: #f1f1f1; 167 | } 168 | 169 | .test-message { 170 | margin-top: 2px; 171 | margin-bottom: 8px; 172 | } 173 | 174 | pre { 175 | margin: 0px; 176 | word-break: normal; 177 | word-wrap: normal; 178 | overflow-x: scroll; 179 | margin-top: 2px; 180 | margin-bottom: 2px; 181 | } 182 | 183 | pre code { 184 | font-size: 0.8em; 185 | color: #333; 186 | overflow-x: auto; 187 | } 188 | 189 | .actual { 190 | position: relative; 191 | } 192 | 193 | .actual pre { 194 | margin-left: 20px; 195 | } 196 | 197 | .actual .arrow { 198 | position: absolute; 199 | font-size: 0.8em; 200 | top: 4px; 201 | } 202 | 203 | /* errors */ 204 | 205 | .test-error { 206 | border-left: 8px solid #b94848; 207 | } 208 | 209 | .error-prefix { 210 | color: #a94442; 211 | font-weight: bold; 212 | } 213 | 214 | .error-message { 215 | font-size: 0.8em; 216 | } 217 | 218 | .view-stacktrace { 219 | font-size: 0.8em; 220 | color: #888; 221 | } 222 | 223 | /* footer */ 224 | 225 | .footer { 226 | margin-top: 30px; 227 | min-height: 150px; 228 | background-color: #ddd; 229 | } 230 | 231 | .footer .container { 232 | padding-top: 30px; 233 | color: #555; 234 | font-size: 0.85em; 235 | display: flex; 236 | justify-content: center; 237 | } 238 | 239 | .control-key { 240 | display: inline-block; 241 | font-weight: bold; 242 | background-color: white; 243 | padding: 1px 4px; 244 | border-radius: 2px; 245 | } 246 | 247 | /* responsive */ 248 | 249 | @media (min-width: 576px) { 250 | .container { 251 | width: 576px; 252 | } 253 | .controls { 254 | height: 0px; 255 | } 256 | } 257 | 258 | @media (min-width: 768px) { 259 | .container { 260 | width: 768px; 261 | } 262 | } 263 | 264 | /* functionality */ 265 | 266 | .hide-passing .test-passing + .test-fail, 267 | .hide-passing .test-fail + .test-passing{ 268 | margin-top: 0px; 269 | } 270 | 271 | .hide-passing .test-ns, 272 | .hide-passing .test-var, 273 | .hide-passing .test-passing { 274 | display: none; 275 | } 276 | 277 | .hide-passing .test-ns.has-errors, 278 | .hide-passing .test-ns.has-failures, 279 | .hide-passing .test-var.has-errors, 280 | .hide-passing .test-var.has-failures { 281 | display: block; 282 | } 283 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cljs-test-display 2 | 3 | [](https://clojars.org/com.bhauman/cljs-test-display) 4 | 5 | `cljs-test-display` is a library that produces a visual display of an 6 | in-browser ClojureScript test run. 7 | 8 |
9 |
10 | ## Overview
11 |
12 | `cljs-test-display` is a ClojureScript library that you can use
13 | along with your web-based test runner to provide visual and system
14 | feedback for your test runs.
15 |
16 | If you have tests written with `cljs.test` and you can run them in the
17 | browser you can use `cljs-test-display`.
18 |
19 | ## Features
20 |
21 | * Fully compatible with `cljs.test`
22 | * Small understandable codebase
23 | * Only a ClojureScript library, no server-side component
24 | * No dependencies
25 |
26 | * Red/green favicon feedback
27 |
28 |
29 |
30 | * Numbered stacktraces for test exceptions in the dev console
31 |
32 |
33 |
34 | * Hide/show passing tests with a key-press
35 |
36 |
37 |
38 | * System notifications for passing and failing test runs
39 |
40 |
41 |
42 | * Straightforward integration
43 |
44 | ```clojure
45 | ;; where "app" is the HTML node where you want to mount the tests
46 | (cljs.test/run-tests
47 | (cljs-test-display.core/init! "app") ;;<-- initialize cljs-test-display here
48 | 'example.foo-test
49 | 'example.bar-test
50 | 'example.baz-test)
51 | ```
52 |
53 | # Usage
54 |
55 | > You will need to be familiar with how to create a ClojureScript
56 | > application and run it in a browser.
57 |
58 | ### Dependencies
59 |
60 | You will need to add `[com.bhauman/cljs-test-display "0.1.1"]` to your
61 | project's dependencies *along with* a recent version of
62 | ClojureScript. It has been tested with
63 | `[org.clojure/cojurescript 1.10.238]` and above, but it should work
64 | with almost any version of ClojureScript that includes `cljs.test`.
65 |
66 | ### Test runner integration
67 |
68 | First, you will need to require `cljs-test-display.core` in your test
69 | runner, then call the `cljs-test-display.core/init!` function.
70 | `init!` returns a `cljs.test` environment much like
71 | `cljs.test/empty-env` initialized so that `cljs-test-display` is
72 | engaged.
73 |
74 | Example: `test/example/test_runner.cljs`
75 |
76 | ```clojure
77 | (ns example.test-runner
78 | (:require
79 | [cljs.test]
80 | [cljs-test-display.core]
81 | [example.foo-test]
82 | [example.bar-test]
83 | [example.baz-test])
84 | (:require-macros
85 | [cljs.test]))
86 |
87 | (defn test-run []
88 | ;; where "app" is the HTML node where you want to mount the tests
89 | (cljs.test/run-tests
90 | (cljs-test-display.core/init! "app") ;;<-- initialize cljs-test-display here
91 | 'example.foo-test
92 | 'example.bar-test
93 | 'example.baz-test))
94 | ```
95 |
96 | Providing `init!` the element id is optional: `app` is the default.
97 |
98 | It is important to note that the `cljs-test-display.core/init!`
99 | function is designed to be called repeatedly in the same environment,
100 | to facilitate hot reloading and test re-runs.
101 |
102 | > For the best development experience, invoke your test
103 | > runner after every hot reload.
104 |
105 | ### HTML host file
106 |
107 | The HTML that hosts the tests can be very simple.
108 |
109 | Example `resources/public/tests.html` file:
110 |
111 | ```html
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ```
121 |
122 | ## Configuration
123 |
124 | You can configure `cljs-test-display` by adding keys to the
125 | `:closure-defines` key in your ClojureScript compiler options.
126 |
127 | ```clojure
128 | {:main example.core
129 | :output-to "main.js"
130 | ...
131 | :closure-defines {
132 | ;; set the element id of where the tests will mount
133 | cljs-test-display.core/root-node-id "test-app" ;; default "app"
134 |
135 | ;; disable the favicon changing behavior
136 | cljs-test-display.core/change-favicon false ;; default true
137 |
138 | ;; disable the system notifications
139 | cljs-test-display.core/notifications false ;; default true
140 |
141 | ;; enable the printing of test results
142 | cljs-test-display.core/printing true ;; default false
143 | }}
144 | ```
145 |
146 | ### Providing your own style
147 |
148 | You can override the injected CSS by supplying your own CSS via an HTML tag
149 | with an id `cljs-test-display-style`.
150 |
151 | For Example:
152 |
153 | ```html
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | ;; you can also place CSS here if you only wish to ammend the CSS
162 |
163 |
164 |
165 |
166 |
167 | ```
168 |
169 |
170 | ## Development
171 |
172 | You should be able to work on `cljs-test-display` by forking/cloning
173 | this repo and then `cd`ing into the `cljs-test-display` directory and
174 | running.
175 |
176 | clojure -A:build
177 |
178 | This will auto-compile and send all changes to the browser without the
179 | need to reload. After the compilation process is complete, you will
180 | get a Browser Connected REPL. An easy way to try it is:
181 |
182 | (js/alert "Am I connected?")
183 |
184 | This should cause an alert to pop up in the browser window.
185 |
186 | You will now be able to live edit the code in
187 | `src/cljs-test-display/core.cljs` and live edit the CSS in
188 | `resources/public/com/bhauman/cljs-test-display/css/style.css`.
189 |
190 | To clean all compiled files:
191 |
192 | rm -rf target/public
193 |
194 | ## License
195 |
196 | Copyright © 2018 Bruce Hauman
197 |
198 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
199 |
--------------------------------------------------------------------------------
/src/cljs_test_display/core.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-test-display.core
2 | (:require
3 | [cljs.test :refer [report inc-report-counter!
4 | testing-contexts-str
5 | testing-vars-str
6 | get-current-env]
7 | :include-macros true]
8 | [clojure.string :as string]
9 | [goog.dom :as gdom]
10 | [goog.dom.classlist :as classlist]
11 | [goog.events :as events]
12 | [goog.events.EventType :as evtype]
13 | [goog.events.KeyCodes :as key-codes]
14 | [cljs-test-display.favicon :as favicon]
15 | [cljs-test-display.notify :as notify])
16 | (:require-macros
17 | [cljs-test-display.core :refer [css]]))
18 |
19 | ;; ------------------------------------------------------------
20 | ;; State Management
21 | ;; ------------------------------------------------------------
22 |
23 | ;; root-node-id specifies the id of the dom element to mount the
24 | ;; testing application to
25 | (goog-define root-node-id "app")
26 |
27 | ;; change-favicon specifies wether to change the favicon to red or
28 | ;; green when tests complete
29 | (goog-define change-favicon true)
30 |
31 | ;; notifications specifies wether to use the Web Notification API to
32 | ;; to provide feedback about test results
33 | (goog-define notifications true)
34 |
35 | ;; printing specifies wether to print out test results in the console
36 | ;; as well
37 | (goog-define printing false)
38 |
39 | (defonce state (atom {}))
40 |
41 | (defn root-app-node [] (gdom/getElement root-node-id))
42 |
43 | (defn push-node! [node]
44 | (swap! state update :current-node (fnil conj (list)) node))
45 |
46 | (defn pop-node! []
47 | (swap! state update :current-node rest))
48 |
49 | (defn initialize-state! []
50 | (reset! state {})
51 | (push-node! (root-app-node)))
52 |
53 | (defn current-node []
54 | (first (get @state :current-node)))
55 |
56 | (defn current-node-parent []
57 | (second (get @state :current-node)))
58 |
59 | (defn next-error-count []
60 | (:error-count (swap! state update :error-count (fnil inc 0))))
61 |
62 | (declare add-header-node! click-toggle n)
63 |
64 | (defonce register-document-events!
65 | (memoize
66 | (fn []
67 | (events/listen (gdom/getDocument)
68 | evtype/KEYDOWN
69 | (fn [e]
70 | (when (= (.-keyCode e) key-codes/P)
71 | (click-toggle e)))))))
72 |
73 | (let [style-string (css)]
74 | (defn insert-style! []
75 | (when-not (gdom/getElement "cljs-test-display-style")
76 | (let [node (n :style
77 | {:id "cljs-test-display-style"}
78 | style-string)
79 | head (aget (gdom/getElementsByTagName "head") 0)]
80 | (gdom/appendChild head node)))))
81 |
82 | #_ (insert-style!)
83 |
84 | ;; ------------------------------------------------------------
85 | ;; DOM Node Creation
86 | ;; ------------------------------------------------------------
87 |
88 | (defn n [tag attributes & contents]
89 | (apply gdom/createDom (name tag) (clj->js attributes)
90 | (mapv
91 | (fn [x]
92 | (cond
93 | (instance? js/HTMLElement x)
94 | x
95 | (or (string? x) (symbol? x))
96 | (gdom/createTextNode (str x))
97 | :else
98 | (gdom/createTextNode (pr-str x))))
99 | (filter some? contents))))
100 |
101 | (defn magic-keyword->attrs [k]
102 | (if (keyword? k)
103 | (let [classes (string/split (name k) ".")
104 | [id classes] (if (string/starts-with? (first classes) "#")
105 | [(subs (first classes) 1) (rest classes)]
106 | [nil classes])]
107 | (cond-> {}
108 | id (assoc :id id)
109 | (not-empty classes) (assoc :class (string/join " " classes))))
110 | {}))
111 |
112 | (defn tag-fn [tag]
113 | (fn [klass & contents]
114 | (let [[klass contents] (if (keyword? klass)
115 | [klass contents]
116 | [nil (cons klass contents)])]
117 | (apply n tag (magic-keyword->attrs klass) contents))))
118 |
119 | (def div (tag-fn :div))
120 | (def span (tag-fn :span))
121 |
122 | (defn code
123 | ([code-str] (code nil code-str))
124 | ([klass code-str]
125 | (n :pre (magic-keyword->attrs klass)
126 | (n :code {} (pr-str code-str)))))
127 |
128 | ;; ------------------------------------------------------------
129 | ;; Data Helpers
130 | ;; ------------------------------------------------------------
131 |
132 | (defn current-var-info []
133 | (let [var (first (reverse (:testing-vars (get-current-env))))]
134 | (meta var)))
135 |
136 | (defn process-file-name [{:keys [ns file]}]
137 | (string/join "/"
138 | (reverse
139 | (take (inc (count (string/split (name ns) #"\.")))
140 | (reverse (string/split file #"[/\\]"))))))
141 |
142 | (defn failed? [m]
143 | (not (zero? (+ (:fail m) (:error m)))))
144 |
145 | (defn pluralize [s n]
146 | (if (= n 1) s (str s "s")))
147 |
148 | ;; ------------------------------------------------------------
149 | ;; DOM Templates
150 | ;; ------------------------------------------------------------
151 |
152 | ;; ------------------------------------------------------------
153 | ;; Header
154 |
155 | (defn click-toggle [e]
156 | (classlist/toggle (root-app-node) "hide-passing"))
157 |
158 | (defn header-node []
159 | (div :#report-header
160 | (div :container.report-body
161 | (div :page-title
162 | (n :img
163 | {:width 50
164 | :height 50
165 | :src "https://clojurescript.org/images/cljs-logo-120b.png"})
166 | (div :test-title "Test Run"))
167 | (div :#summary))))
168 |
169 | (defn add-header-node! []
170 | (gdom/appendChild (root-app-node) (header-node))
171 | (gdom/appendChild (root-app-node)
172 | (div :controls.container
173 | (n :button {:id "hide-show"
174 | :onclick
175 | click-toggle}
176 | "Hide/Show Passing"))))
177 |
178 | ;; ------------------------------------------------------------
179 | ;; Failure
180 |
181 | (defn contexts-node []
182 | (when (seq (:testing-contexts (get-current-env)))
183 | (div :contexts (testing-contexts-str))))
184 |
185 | (defn comparison [{:keys [actual expected]}]
186 | (div
187 | (code expected)
188 | (div :actual (div :arrow "▶") (code actual))))
189 |
190 | (defn add-fail-node! [m]
191 | (let [formatter-fn (or (:formatter (get-current-env)) pr-str)
192 | node (div :test-fail
193 | (contexts-node)
194 | (div :fail-body
195 | (when-let [message (:message m)]
196 | (div :test-message message))
197 | (comparison m)))
198 | curr-node (current-node)]
199 | (classlist/add curr-node "has-failures")
200 | (classlist/add (current-node-parent) "has-failures")
201 | (gdom/appendChild curr-node node)))
202 |
203 | ;; ------------------------------------------------------------
204 | ;; Error
205 |
206 | (defn error-comparison [{:keys [expected actual]}]
207 | (div
208 | (code expected)
209 | (div :cljs-test-actual
210 | (span :error-prefix "Error: ")
211 | (when actual
212 | (span :error-message (.-message actual)))
213 | (when actual
214 | (let [error-number (next-error-count)]
215 | (js/console.log "CLJS Test Error #" error-number)
216 | (js/console.error actual)
217 | (div :view-stacktrace
218 | (str "For stacktrace: See error number " error-number " in console")))))))
219 |
220 | (defn add-error-node! [m]
221 | (let [formatter-fn (or (:formatter (get-current-env)) pr-str)
222 | node (div :test-fail.test-error
223 | (contexts-node)
224 | (div :fail-body
225 | (when-let [message (:message m)]
226 | (div :test-message message))
227 | (error-comparison m)))
228 | curr-node (current-node)]
229 | (classlist/add curr-node "has-errors")
230 | (classlist/add (current-node-parent) "has-errors")
231 | (gdom/appendChild curr-node node)))
232 |
233 | ;; ------------------------------------------------------------
234 | ;; Passing
235 |
236 | (defn add-passing-node! [m]
237 | (gdom/appendChild (current-node) (div :test-passing)))
238 |
239 | ;; ------------------------------------------------------------
240 | ;; NS
241 |
242 | (defn add-ns-node! [m]
243 | (let [curr-node (current-node)
244 | new-current-node
245 | (div :container.test-ns
246 | (n :h2 {} (:ns m)))]
247 | (swap! state update :current-node #(cons new-current-node %))
248 | (gdom/appendChild
249 | curr-node
250 | new-current-node)))
251 |
252 | ;; ------------------------------------------------------------
253 | ;; Var
254 |
255 | (defn add-var-node [m]
256 | (let [curr-node (current-node)
257 | {:keys [name line file] :as info} (current-var-info)
258 | node
259 | (div :test-var
260 | (div :var-header
261 | (str "/" name)
262 | (when line
263 | (div :test-var-line (if file
264 | (process-file-name info)
265 | "line") ":"
266 | (n :span {} line)))))]
267 | (swap! state update :current-node #(cons node %))
268 | (gdom/appendChild curr-node node)))
269 |
270 | ;; ------------------------------------------------------------
271 | ;; Summary
272 |
273 | (defn summary [{:keys [fail error pass test] :as m}]
274 | (div :summary-body
275 | (when (not (zero? fail))
276 | (div :report-number (str fail (pluralize " failure" fail))))
277 | (when (not (zero? error))
278 | (div :report-number (str error (pluralize " error" error))))
279 | (when-not (failed? m)
280 | (div :report-number "All Tests Passed"))
281 | (div :total-tests
282 | (div "Totals")
283 | (div :test-counts
284 | (div (str test (pluralize " Test" test)))
285 | (let [assertions (+ pass fail error)]
286 | (div (str assertions (pluralize " Assertion" assertions))))))))
287 |
288 | (defn display-summary! [m]
289 | (let [report-header (gdom/getElement "report-header")
290 | summary-node' (gdom/getElement "summary")]
291 | (classlist/add report-header (if (failed? m)
292 | "tests-fail"
293 | "tests-succeed"))
294 | (gdom/removeChildren summary-node')
295 | (gdom/appendChild summary-node' (summary m))))
296 |
297 | ;; ------------------------------------------------------------
298 | ;; Hooking into cljs.test/report
299 | ;; ------------------------------------------------------------
300 |
301 | (defn print-comparison [m]
302 | (let [formatter-fn (or (:formatter (get-current-env)) pr-str)]
303 | (println "expected:" (formatter-fn (:expected m)))
304 | (println " actual:" (formatter-fn (:actual m)))))
305 |
306 | (defmethod report [::default :pass] [m]
307 | (add-passing-node! m)
308 | (inc-report-counter! :pass))
309 |
310 | ;; namespace start and end
311 |
312 | (defmethod report [::default :begin-test-ns] [m]
313 | (add-ns-node! m)
314 | (when printing
315 | (println "\nTesting" (name (:ns m)))))
316 |
317 | (defmethod report [::default :end-test-ns] [m]
318 | (swap! state update :current-node rest))
319 |
320 | ;; var start and end
321 |
322 | (defmethod report [::default :begin-test-var] [m]
323 | (add-var-node m))
324 |
325 | (defmethod report [::default :end-test-var] [m]
326 | (swap! state update :current-node rest))
327 |
328 | ;; failure and errors
329 |
330 | (defmethod report [::default :fail] [m]
331 | (add-fail-node! m)
332 | (inc-report-counter! :fail)
333 | (when printing
334 | (println "\nFAIL in" (testing-vars-str m))
335 | (when (seq (:testing-contexts (get-current-env)))
336 | (println (testing-contexts-str)))
337 | (when-let [message (:message m)] (println message))
338 | (print-comparison m)))
339 |
340 | (defmethod report [::default :error] [m]
341 | (inc-report-counter! :error)
342 | (println "\nERROR in" (testing-vars-str m))
343 | (when (seq (:testing-contexts (get-current-env)))
344 | (println (testing-contexts-str)))
345 | (when-let [message (:message m)] (println message))
346 | (print-comparison m)
347 | ;; display AFTER so that error shows up in console after the printed error
348 | (add-error-node! m))
349 |
350 | ;; Ignore these but keep them as a reference
351 | #_(defmethod report [::default :end-run-tests] [m])
352 | #_(defmethod report [::default :end-test-all-vars] [m])
353 | #_(defmethod report [::default :end-test-vars] [m])
354 |
355 | ;; summary
356 |
357 | (defmethod report [::default :summary] [m]
358 | (when change-favicon
359 | (if (failed? m)
360 | (favicon/red)
361 | (favicon/green)))
362 | (when notifications
363 | (if (failed? m)
364 | (notify/failure m)
365 | (notify/success)))
366 | (display-summary! m)
367 | (gdom/appendChild (root-app-node)
368 | (div :footer
369 | (div :container
370 | (div :tip
371 | "Hit the "
372 | (span :control-key "P")
373 | " key to toggle the display of passing tests."))))
374 | (when printing
375 | (println "\nRan" (:test m) "tests containing"
376 | (+ (:pass m) (:fail m) (:error m)) "assertions.")
377 | (println (:fail m) "failures," (:error m) "errors.")))
378 |
379 | ;; ------------------------------------------------------------
380 | ;; Main API
381 | ;; ------------------------------------------------------------
382 |
383 | (defn empty-env []
384 | (assoc (cljs.test/empty-env) :reporter ::default))
385 |
386 | (defn init!
387 | "This function initializes the environment for a test run. It must
388 | be called before every test run.
389 |
390 | As a convenience it returns a cljs.test/empty-env initialized so
391 | that the test run will use the cljs-test-display formatter.
392 |
393 | This function takes an optional single argument: the id of the DOM
394 | node to mount. It defaults to \"app\"
395 |
396 | Example Usage:
397 |
398 | (cljs.test/run-tests (cljs-test-display/init! \"app\")
399 | 'example.core-test
400 | 'example.core-other-test)"
401 | ([] (init! nil))
402 | ([app-node-id]
403 | (if (nil? goog/global.document) ;; if not in HTML env ingore display
404 | (cljs.test/empty-env)
405 | (do
406 | (when app-node-id
407 | (assert (or (string? app-node-id)
408 | (symbol? app-node-id)
409 | (keyword? app-node-id))
410 | "Must provide an something we can call cljs.core/name on.")
411 | (set! root-node-id (name app-node-id)))
412 | (assert (gdom/getElement (name root-node-id))
413 | (str "cljs-test-display: Element with id "
414 | (pr-str root-node-id)
415 | " does not exist."))
416 | (when notifications (notify/ask-permission!))
417 | (insert-style!)
418 | (register-document-events!)
419 | (set! (.-innerHTML (root-app-node)) "")
420 | (add-header-node!)
421 | (initialize-state!)
422 | (empty-env)))))
423 |
--------------------------------------------------------------------------------