├── .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 | [![Clojars Project](https://img.shields.io/clojars/v/com.bhauman/cljs-test-display.svg)](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 | --------------------------------------------------------------------------------