├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.adoc ├── bin └── release ├── circle.yml ├── dev ├── clj │ └── user.clj └── cljs │ └── user.cljs ├── docs └── index.adoc ├── package.json ├── project.clj ├── resources ├── private │ └── unit-tests.html └── public │ ├── css │ └── untangled-spec-styles.css │ ├── index.html │ ├── untangled-spec-client-tests.html │ └── untangled-spec-server-tests.html ├── src └── untangled_spec │ ├── assertions.cljc │ ├── async.cljc │ ├── contains.cljc │ ├── core.clj │ ├── core.cljs │ ├── diff.cljc │ ├── dom │ └── edn_renderer.cljs │ ├── impl │ ├── macros.clj │ ├── runner.clj │ └── selectors.cljc │ ├── provided.clj │ ├── renderer.cljs │ ├── reporter.cljc │ ├── reporters │ ├── console.cljs │ ├── suite.clj │ └── terminal.clj │ ├── router.cljs │ ├── runner.cljc │ ├── selectors.cljc │ ├── spec.cljc │ ├── spec_renderer.cljs │ ├── stub.cljc │ ├── suite.cljc │ └── watch.clj └── test └── untangled_spec ├── all_tests.cljs ├── assertions_spec.clj ├── assertions_spec.cljs ├── async_spec.cljc ├── contains_spec.cljc ├── core_spec.clj ├── diff_spec.cljc ├── provided_spec.cljc ├── selectors_spec.cljc ├── stub_spec.cljc ├── testing_helpers.cljc ├── tests_to_run.cljs └── timeline_spec.cljc /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.sw? 3 | /target 4 | /classes 5 | /checkouts 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | /.lein-* 11 | /.nrepl-port 12 | .hgignore 13 | .hg/ 14 | .idea 15 | *.iml 16 | figwheel_server.log 17 | resources/public/js 18 | out 19 | node_modules/ 20 | resources/private/js 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: 3 | - make tests 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ----- 3 | - Adding ability to render clojure tests in the browser! 4 | - WIP: when-mocking & provided will use clojure.spec to verify the :args passed to mocks, the :ret value you specify, and their relationship if a :fn spec if they exist. 5 | - Adding selectors to the specification macro that work with untangled-spec, test-refresh, doo, etc... 6 | - They emit meta data on the deftest var so they are compatible with anything, 7 | but also wrap the body so that untangled-spec can properly run just the selected tests. 8 | - gh-30 -> only showing non-redundant diffs 9 | 10 | 0.4.0 11 | ----- 12 | Lots of bug/issue fixes: 13 | - gh-6 -> fixed by work in gh-17 14 | - gh-8 -> using edn/read-string on "[" m "]" or falling back to just the message itself 15 | - gh-10 -> using js/alert for now 16 | - gh-11 -> by adding fix-str which handles nil and empty string as special cases 17 | - gh-13 -> adding test selector support in specification macro 18 | - gh-16 -> by wrapping the generated mock code in do-report's 19 | - gh-17 -> by using (& tweaking) diff/patch in the edn renderer 20 | - gh-18 -> can now capture varargs in a stub! 21 | - gh-21 -> added history to stubs for use in debugging when the stub is not called the required number of times 22 | - gh-21 -> improved messaging for when a stub is called with invalid arguments (ie: count mismatch or failing literal) 23 | - gh-21 -> reporting failing arguments when a stub is called too many times 24 | - gh-21 -> when validating stubs, failures will contain in their ex-data, the whole script, and each step will now also have history, (for cases when you are mocking a function with multiple steps, eg: =1x=> ... =2x=>) 25 | - gh-23 -> =throws=> now additionally supports a symbol, OR a map with optional keys: :ex-type :regex :fn 26 | - gh-25 -> added pending & passing filters 27 | - gh-28 -> using clojure.spec to parse provided & when-mocking 28 | - gh-32 -> fixing broken conform 29 | 30 | Assorted fixes/improvements: 31 | - added exception handling to diff rendering 32 | - fixing stub arg validation to allow for no literals 33 | - fixing weird bug with diff patch where different size vectors crash the patch fn 34 | 35 | 0.3.9 36 | ----- 37 | - Fixed bug with diff algorithm, see github issue #3 38 | 39 | 0.3.8 40 | ----- 41 | - Fixed bug with diff algorithm, see github issue #2 42 | 43 | 0.3.7 44 | ----- 45 | - Updated to work with React 15 and Om 36 46 | 47 | 0.3.6 - April 22, 2016 48 | ----- 49 | - Added untangled-spec.reporters.terminal/merge-cfg! 50 | - No arguments will print the valid keys, and if you pass it a map it will 51 | verify that you are only modifying existing key-value pairs. 52 | - Adding gensym to the symbol specification generates for deftest. 53 | - Conflicts with specification names & any other vars are now impossible 54 | - Can now configure pprint *print-level* & *print-depth* using untangled-spec.reporters.terminal/merge-cfg! 55 | 56 | 0.1.1 57 | ----- 58 | - Added support for new macros: 59 | - `when-mocking`: Just like `provided`, but without output (no string parameter) 60 | - `component`: Alias for `behavior`. Makes specs read better. 61 | - `behavior` macro now supports :manual-test as a body, which will indicate that the test requires a human to do the steps. 62 | 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 NAVIS 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tests: 2 | npm install 3 | lein test-cljs 4 | lein test-clj 5 | 6 | deploy: 7 | lein with-profile with-cljs deploy clojars 8 | 9 | help: 10 | @ make -rpn | sed -n -e '/^$$/ { n ; /^[^ ]*:/p; }' | sort | egrep --color '^[^ ]*:' 11 | 12 | .PHONY: tests help 13 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = untangled-spec 2 | :source-highlighter: coderay 3 | :source-language: clojure 4 | :toc: 5 | :toc-placement: preamble 6 | :sectlinks: 7 | :sectanchors: 8 | :sectnums: 9 | 10 | ifdef::env-github[] 11 | :tip-caption: :bulb: 12 | :note-caption: :information_source: 13 | :important-caption: :heavy_exclamation_mark: 14 | :caution-caption: :fire: 15 | :warning-caption: :warning: 16 | endif::[] 17 | 18 | A Specification testing framework. 19 | 20 | NOTE: THE LAST NON-ALPHA RELEASE WAS link:https://github.com/untangled-web/untangled-spec/tree/0.4.0[Version 0.4.0]. 21 | 22 | image::https://img.shields.io/clojars/v/navis/untangled-spec.svg[link="https://clojars.org/navis/untangled-spec"] 23 | 24 | Release: image:https://api.travis-ci.org/untangled-web/untangled-spec.svg?branch=master[link=https://github.com/untangled-web/untangled-spec/tree/master] 25 | Develop: image:https://api.travis-ci.org/untangled-web/untangled-spec.svg?branch=develop[link=https://github.com/untangled-web/untangled-spec/tree/develop] 26 | 27 | == Usage 28 | 29 | * Make sure your link:https://clojure.org/community/downloads[clojure](link:https://github.com/clojure/clojurescript/releases[script]) versions are at or above "1.9.x". 30 | 31 | * Add `[navis/untangled-spec "x.y.z"]` to your `:dependencies`. 32 | 33 | * Make sure you have at least one test file, eg: `test/your-ns/arithmetic_spec.cljc`, that uses `untangled-spec.core`: 34 | 35 | [source] 36 | ---- 37 | (ns your-ns.arithmetic-spec 38 | (:require 39 | [untangled-spec.core :refer [specification behavior component assertions]])) 40 | 41 | (specification "arithmetic" 42 | (component "addition" 43 | (behavior "is commutative" 44 | (assertions 45 | (+ 13 42) => (+ 42 13))))) 46 | ---- 47 | 48 | === Clojure In The Terminal 49 | 50 | * Add `[com.jakemccrary/lein-test-refresh "x.y.z"]` to your `:plugins`. 51 | ** Check link:https://github.com/jakemcc/lein-test-refresh#usage[lein test refresh itself] for the latest version. 52 | * Add the following to your `project.clj` configuration: 53 | 54 | :test-refresh {:report untangled-spec.reporters.terminal/untangled-report} 55 | 56 | [NOTE] 57 | ==== 58 | Other configuration options are available, take a look at: 59 | 60 | * link:https://github.com/jakemcc/lein-test-refresh/blob/master/sample.project.clj[]. 61 | * link:https://github.com/jakemcc/lein-test-refresh/blob/master/test-refresh/src/leiningen/test_refresh.clj[]. 62 | ==== 63 | 64 | * Run `lein test-refresh` in your command-line, et voila! You should see something like: 65 | 66 | ---- 67 | Using reporter: untangled-spec.reporters.terminal/untangled-report 68 | ********************************************* 69 | *************** Running tests *************** 70 | :reloading (your-ns.arithmetic-spec) 71 | Running tests for: (your-ns.arithmetic-spec) 72 | 73 | Testing your-ns.arithmetic-spec 74 | addition 75 | is commutative 76 | 77 | Ran 1 tests containing 1 assertions. 78 | 0 failures, 0 errors. 79 | 80 | Failed 0 of 1 assertions 81 | Finished at 17:32:43.925 (run time: 0.01s) 82 | ---- 83 | 84 | TIP: Make sure you make the test fail to check that error reporting is working before moving on to another section. 85 | 86 | [WARNING] 87 | ==== 88 | 89 | Error refreshing environment: java.io.FileNotFoundException: Could not locate clojure/spec__init.class or clojure/spec.clj on classpath. 90 | 91 | Make sure you have link:https://clojure.org/community/downloads[clojure](link:https://github.com/clojure/clojurescript/releases[script]) versions above "1.9.x". 92 | ==== 93 | 94 | [WARNING] 95 | ==== 96 | 97 | Error refreshing environment: java.lang.IllegalAccessError: clj does not exist, compiling:(untangled_spec/watch.clj:1:1) 98 | 99 | Add an `:exclusions [org.clojure/tools.namespace]` for tools.namespace on lein-test-refresh + 100 | (and any other projects that use it, which you can check using `lein deps :tree` or `boot -pd`), + 101 | as untangled-spec requires "0.3.x" for clojurescript support, but lein-test-refresh doesn't need that itself. 102 | ==== 103 | 104 | === Clojure In The Browser 105 | 106 | * Create a `dev/clj/user.clj` file that contains: 107 | 108 | [source] 109 | ---- 110 | (ns clj.user 111 | (:require 112 | [untangled-spec.selectors :as sel] 113 | [untangled-spec.suite :as suite]) 114 | 115 | (suite/def-test-suite my-test-suite 116 | {:config {:port 8888} ;;<2> 117 | :test-paths ["test"] 118 | :source-paths ["src"]} 119 | {:available #{:focused :unit :integration} 120 | :default #{::sel/none :focused :unit}}) 121 | 122 | (my-test-suite) ;;<1> 123 | ---- 124 | <1> Starts the test suite, note that it will stop any pre-existing test suite first, so it's safe to call this whenever (eg: hot code reload). 125 | <2> You can now goto link:localhost:8888/untangled-spec-server-tests.html[] 126 | 127 | //DIVIDER WHY OH WHY 128 | * Make sure the `"dev"` folder is in your `:source-paths`, if you are using lein that's probably just a `:profiles {:dev {:source-paths ["dev"]}}`. 129 | * Add `clj.user` to your `:repl-options {:init-ns clj.user}`, which again if using lein probably goes in your `:profiles {:dev #_...}` 130 | 131 | === CLJS In The Browser 132 | 133 | * Add `[figwheel-sidecar "x.y.z"]` to your `dev` time dependencies (link:https://clojars.org/lein-figwheel[latest releases]). 134 | ** Add `[com.cemerick/piggieback "x.y.z"]` to your `dev` time dependencies (link:https://clojars.org/com.cemerick/piggieback[latest version]). 135 | ** Add `:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]` to your `:repl-options`. 136 | * Add `[org.clojure/clojurescript "x.y.z"]` as a normal dependencies (link:https://github.com/clojure/clojurescript/releases[latest releases]). 137 | 138 | * Add to your `/dev/clj/user.clj`: 139 | 140 | [source] 141 | ---- 142 | (:require 143 | [com.stuartsierra.component :as cp] 144 | [figwheel-sidecar.system :as fsys] 145 | #_...) 146 | 147 | (defn start-figwheel [build-ids] 148 | (-> (fsys/fetch-config) 149 | (assoc-in [:data :build-ids] build-ids) 150 | fsys/figwheel-system cp/start fsys/cljs-repl)) 151 | ---- 152 | 153 | * Create a `/dev/cljs/user.cljs` 154 | 155 | [source] 156 | ---- 157 | (ns cljs.user 158 | (:require 159 | your-ns.arithmetic-spec ;;<1> 160 | [untangled-spec.selectors :as sel] 161 | [untangled-spec.suite :as suite])) 162 | 163 | (suite/def-test-suite on-load {:ns-regex #"your-ns\..*-spec"} ;;<2> 164 | {:default #{::sel/none :focused} 165 | :available #{:focused :should-fail}}) 166 | ---- 167 | <1> Ensures your tests are loaded so the test suite can find them 168 | <2> Regex for finding just your tests from all the loaded namespaces. 169 | 170 | * (Optional) Create an HTML file for loading your tests in your `resources/public` folder. If you're using 171 | the standard figwheel config, then you can also choose to load one that is 172 | provided in the JAR of Untangled Spec. 173 | 174 | [source,html] 175 | ---- 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
Loading "js/test/test.js", if you need to name that something else (conflicts?) make your own test html file
186 | 187 | 188 | 189 | ---- 190 | 191 | The HTML above is exactly the content of the built-in file 192 | `untangled-spec-client-tests.html`. 193 | 194 | //DIVIDER WHY OH WHY 195 | * Add `[lein-cljsbuild "x.y.z"]` as a `:plugin` (link:https://github.com/emezeske/lein-cljsbuild#latest-version[latest version]). 196 | * Add a `:cljsbuild` for your tests (link:https://github.com/emezeske/lein-cljsbuild#basic-configuration[basic configuration]), eg: 197 | 198 | [source] 199 | ---- 200 | :cljsbuild {:builds [ 201 | 202 | {:id "test" 203 | :source-paths ["src" "dev" "test"] 204 | :figwheel {:on-jsload cljs.user/on-load} 205 | :compiler {:main cljs.user 206 | :output-to "resources/public/js/test/test.js" 207 | :output-dir "resources/public/js/test/out" 208 | :asset-path "js/test/out" 209 | :optimizations :none}} 210 | 211 | ]} 212 | ---- 213 | 214 | lein repl 215 | #_=> (start-figwheel ["test"]) 216 | 217 | [WARNING] 218 | ==== 219 | java.lang.RuntimeException: No such var: om/dispatch, compiling:(untangled/client/mutations.cljc:8:1) 220 | 221 | Means you have a conflicting org.omcljs/om versions, either resolve them by looking at `lein deps :tree` or `bood -pd`, or pin your version to the link:https://github.com/omcljs/om/releases[latest version] or whatever version untangled-spec is using. 222 | ==== 223 | 224 | * Run the tests by loading your HTML file (or the one provided in the Untangled Spec JAR). The default figwheel 225 | port is 3449, so the URL that should always work by default if you've named your 226 | javascript output `js/test/test.js` would be: link:http://localhost:3449/untangled-spec-client-tests.html[] 227 | 228 | 229 | ==== For CI 230 | 231 | * Add lein-doo as both a test dependency and a plugin 232 | 233 | :dependencies [#_... [lein-doo "0.1.6" :scope "test"] #_...] 234 | :plugins [#_... [lein-doo "0.1.6"] #_...] 235 | 236 | * Add a `:doo` section to your project.clj 237 | 238 | :doo {:build "automated-tests" 239 | :paths {:karma "node_modules/karma/bin/karma"}} 240 | 241 | * Add a top level `package.json` containing at least: 242 | 243 | { 244 | "devDependencies": { 245 | "karma": "^0.13.19", 246 | "karma-chrome-launcher": "^0.2.2", 247 | "karma-firefox-launcher": "^0.1.7", 248 | "karma-cljs-test": "^0.1.0" 249 | } 250 | } 251 | 252 | * Add a `:cljsbuild` for your CI tests, eg: 253 | 254 | [source] 255 | ---- 256 | :cljsbuild {:builds [ 257 | 258 | {:id "automated-tests" 259 | :source-paths ["src" "test"] 260 | :compiler {:output-to "resources/private/js/unit-tests.js" 261 | :output-dir "resources/private/js/unit-tests" 262 | :asset-path "js/unit-tests" 263 | :main untangled-spec.all-tests 264 | :optimizations :none}} 265 | 266 | ]} 267 | ---- 268 | 269 | * Add a file that runs your tests 270 | 271 | [source] 272 | ---- 273 | (ns your-ns.all-tests 274 | (:require 275 | your-ns.arithmetic-spec ;; ensures tests are loaded so doo can find them 276 | [doo.runner :refer-macros [doo-all-tests]])) 277 | 278 | (doo-all-tests #"untangled-spec\..*-spec") 279 | ---- 280 | 281 | * Run `npm install` & then `lein doo chrome automated-tests once`, + 282 | 283 | NOTE: If you put the `automated-tests` build in a lein profile (eg: test), + 284 | you will have to prepend a `with-profile test ...` in your command. 285 | 286 | * See link:http://github.com/bensu/doo#doo[doo] itself for further details & as a fallback if this information is somehow out of date. 287 | 288 | == Learn More 289 | * about link:docs/index.adoc#untangled-spec-docs[Untangled Spec] 290 | * about link:http://untangled-web.github.io/untangled/index.html[Untangled] & checkout the link:http://untangled-web.github.io/untangled/index.html[Documentation Reference] 291 | * interactively with the link:http://untangled-web.github.io/untangled/tutorial.html[Untangled Tutorial] 292 | ** http://untangled-web.github.io/untangled/tutorial.html#!/untangled_tutorial.K_Testing[untangled_tutorial.K_Testing] 293 | 294 | == Development 295 | 296 | NOTE: This section is for the _development_ of *untangled-spec itself*. + 297 | If you wanted instructions on how to use untangled-spec in your app/library, see <> 298 | 299 | === CLJS In The Browser 300 | 301 | lein repl 302 | #_user=> (start-figwheel ["test"]) 303 | 304 | & link:localhost:8888/untangled-spec-server-tests.html[] 305 | 306 | === Clojure In The Terminal 307 | 308 | lein test-refresh 309 | 310 | === Clojure In The Browser 311 | 312 | lein repl 313 | #_user=> (start) 314 | 315 | & link:localhost:8888/untangled-spec-server-tests.html[] 316 | 317 | === CI Testing 318 | 319 | To run the CLJ and CLJS tests on a CI server, it must have chrome, node, and npm installed. + 320 | Then you can simply use the Makefile: 321 | 322 | make tests 323 | 324 | or manually run: 325 | 326 | npm install 327 | lein test-cljs 328 | lein test-clj 329 | 330 | == License 331 | 332 | MIT License 333 | Copyright © 2015 NAVIS 334 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | set -o pipefail 4 | ([[ "$TRACE" ]] || [[ "$DEBUG" ]]) && set -o xtrace 5 | 6 | assert_committed() { 7 | if [[ -n "$(git status --porcelain)" ]]; then 8 | echo "[release] [WARNING] uncommitted changes detected" 9 | git status 10 | false 11 | fi 12 | } 13 | 14 | get_version() { 15 | echo "$(head -n 1 project.clj | cut -d' ' -f3 | sed 's/"//g')" 16 | } 17 | 18 | task_init() { 19 | get_version | grep -e "SNAPSHOT" 20 | (tail +2 project.clj | grep -e "SNAPSHOT") && false 21 | git checkout develop 22 | git pull 23 | } 24 | 25 | task_test() { 26 | if [[ "${SKIP_TESTS}" ]]; then 27 | echo "[bin/release] WARNING: SKIPPING TESTS" 28 | else 29 | (npm install && 30 | lein doo chrome automated-tests once && 31 | lein test-refresh :run-once) 32 | fi 33 | } 34 | 35 | task_start() { 36 | git flow release start "$(get_version | sed 's/"//g' | sed 's/-SNAPSHOT//')" 37 | } 38 | 39 | task_release() { 40 | git commit --all -m "Automated release commit for: $(get_version)" 41 | git flow release finish -p -m "Automated release finish for: $(get_version)" "$(get_version | sed 's/"//g')" 42 | if [[ "$DEPLOY" ]]; then 43 | lein deploy "$DEPLOY" || true 44 | fi 45 | } 46 | 47 | task_end() { 48 | git commit --all -m "Automated release bump for: $(get_version)" 49 | git push 50 | } 51 | 52 | main() { 53 | local task="$1" 54 | case "$task" in 55 | "version") 56 | get_version 57 | ;; 58 | "init") 59 | task_init 60 | ;; 61 | "test") 62 | task_test 63 | ;; 64 | "start") 65 | task_start 66 | ;; 67 | "release") 68 | task_release 69 | ;; 70 | "end") 71 | task_end 72 | ;; 73 | "all_tasks") 74 | assert_committed 75 | task_init 76 | task_test 77 | task_start 78 | lein change version leiningen.release/bump-version release 79 | task_release 80 | lein change version leiningen.release/bump-version 81 | task_end 82 | ;; 83 | *) 84 | echo "invalid cmd $task" 85 | false 86 | ;; 87 | esac 88 | } 89 | 90 | main "$@" 91 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - npm install 4 | test: 5 | override: 6 | - lein test-clj 7 | - lein test-cljs 8 | -------------------------------------------------------------------------------- /dev/clj/user.clj: -------------------------------------------------------------------------------- 1 | (ns clj.user 2 | (:require 3 | [clojure.tools.namespace.repl :as tools-ns-repl] 4 | [com.stuartsierra.component :as cp] 5 | [figwheel-sidecar.system :as fsys] 6 | [untangled-spec.impl.runner :as ir] 7 | [untangled-spec.suite :as suite] 8 | [untangled-spec.selectors :as sel])) 9 | 10 | (defn start-figwheel 11 | "Start Figwheel on the given builds, or defaults to build-ids in `figwheel-config`." 12 | ([] 13 | (let [props (System/getProperties) 14 | figwheel-config (fsys/fetch-config) 15 | all-builds (->> figwheel-config :data :all-builds (mapv :id))] 16 | (start-figwheel (keys (select-keys props all-builds))))) 17 | ([build-ids] 18 | (let [figwheel-config (fsys/fetch-config) 19 | target-config (-> figwheel-config 20 | (assoc-in [:data :build-ids] 21 | (or (seq build-ids) 22 | (-> figwheel-config :data :build-ids))))] 23 | (-> (cp/system-map 24 | :css-watcher (fsys/css-watcher {:watch-paths ["resources/public/css"]}) 25 | :figwheel-system (fsys/figwheel-system target-config)) 26 | cp/start :figwheel-system fsys/cljs-repl)))) 27 | 28 | ;; WARNING: INTERNAL syntax ONLY, you should not need to use this yourself 29 | ;; instead use (suite/def-test-suite start-tests ..opts.. ..selectors..) 30 | (defn start [] 31 | (reset! ir/runner 32 | (suite/test-suite-internal 33 | {:config {:port 8888} 34 | :source-paths ["src" "dev"] 35 | :test-paths ["test"]} 36 | {:default #{::sel/none :focused} 37 | :available #{:focused :should-fail}}))) 38 | 39 | (defn stop [] 40 | (swap! ir/runner cp/stop) 41 | (reset! ir/runner {})) 42 | 43 | (defn reset [] 44 | (stop) (tools-ns-repl/refresh :after 'clj.user/start)) 45 | 46 | (defn engage [& build-ids] 47 | (start) (start-figwheel build-ids)) 48 | -------------------------------------------------------------------------------- /dev/cljs/user.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.user 2 | (:require 3 | [clojure.spec.test :as st] 4 | [untangled-spec.tests-to-run] 5 | [untangled-spec.suite :as suite] 6 | [untangled-spec.selectors :as sel])) 7 | 8 | (enable-console-print!) 9 | 10 | ;;optional, but can be helpful 11 | (st/instrument) 12 | 13 | ;;define on-load as a fn that re-runs (and renders) the tests 14 | ;;for use by figwheel's :on-jsload 15 | (suite/def-test-suite on-load {:ns-regex #"untangled-spec\..*-spec"} 16 | {:default #{::sel/none :focused} 17 | :available #{:focused :should-fail}}) 18 | -------------------------------------------------------------------------------- /docs/index.adoc: -------------------------------------------------------------------------------- 1 | = Untangled Spec Docs 2 | :source-highlighter: coderay 3 | :source-language: clojure 4 | :toc: 5 | :toc-placement!: 6 | :toclevels: 3 7 | :sectlinks: 8 | :sectanchors: 9 | :sectnums: 10 | 11 | ifdef::env-github[] 12 | :tip-caption: :bulb: 13 | :note-caption: :information_source: 14 | :important-caption: :heavy_exclamation_mark: 15 | :caution-caption: :fire: 16 | :warning-caption: :warning: 17 | endif::[] 18 | 19 | ifdef::env-github[] 20 | toc::[] 21 | endif::[] 22 | 23 | == Features 24 | 25 | The macros in untangled-spec wrap clojure/cljs test, so that you may use any of the features of the core library. 26 | The specification DSL makes it much easier to read the tests, and also includes a number of useful features: 27 | 28 | - Outline rendering 29 | - Left-to-right assertions 30 | - More readable output, such as data structure comparisons on failure (with diff notation as well) 31 | - Real-time refresh of tests on save (client and server) 32 | - Seeing test results in any number of browsers at once 33 | - Mocking of normal functions, including native javascript (but as expected: not macros or inline functions) 34 | - Mocking verifies call sequence and call count 35 | - Mocks can easily verify arguments received 36 | - Mocks can simulate timelines for CSP logic 37 | - Protocol testing support (helps prove network interactions are correct without running the full stack) 38 | 39 | == Client Tests 40 | 41 | A dev-time only entry point for browser test rendering: 42 | 43 | [source] 44 | ---- 45 | (ns cljs.user 46 | (:require 47 | [untangled-spec.tests-to-run] ;;<1> 48 | [untangled-spec.suite :as suite] 49 | [untangled-spec.selectors :as sel])) 50 | 51 | (suite/def-test-suite on-load ;;<2> 52 | {:ns-regex #"untangled-spec\..*-spec"} ;;<3> 53 | {:available #{:focused :should-fail} ;;<4> 54 | :default #{::sel/none :focused}}) ;;<5> 55 | ---- 56 | 57 | <1> link:../test/untangled_spec/tests_to_run.cljs[tests_to_run.cljs] Just requires all of your tests. This is necessary as all cljs test runners search by looking at the loaded namespaces. Since there are two places cljs tests can run from (browser and CI), it makes sense to keep this list in one file. 58 | <2> Define a callback for figwheel to call to re run the tests. 59 | <3> `:ns-regex` is the regex to filter the loaded namespaces down to just your tests. 60 | <4> Is where you define your `:available` selectors, ie: legal/possible values for selectors on your `specification`. 61 | <5> `:default` selectors to use when you haven't yet specified any in the browser ui. 62 | 63 | A cljsbuild in your project.clj (or if using boot: roughly) as follows: 64 | 65 | [source] 66 | ---- 67 | {:source-paths ["src" "dev" "test"] 68 | :figwheel {:on-jsload cljs.user/on-load} ;;<1> 69 | :compiler {:main cljs.user ;;<2> 70 | :output-to "resources/public/js/test/test.js" ;;<3> 71 | :output-dir "resources/public/js/test/out" 72 | :asset-path "js/test/out" 73 | :optimizations :none}} 74 | ---- 75 | <1> References the earlier defined test suite name so figwheel can re-run the tests when it reloads the js. 76 | <2> Namespace entrypoint (eg: `:main`) points to the dev user namespace as described above. 77 | <3> Must compile to a single javascript file at `"resources/public/js/test/test.js"`. 78 | 79 | An entrypoint for starting figwheel (or something equivalent if in boot): 80 | 81 | [source] 82 | ---- 83 | (:require 84 | [com.stuartsierra.component :as cp] 85 | [figwheel-sidecar.system :as fsys]) 86 | 87 | (defn start-figwheel 88 | "Start Figwheel on the given builds, or defaults to build-ids in `figwheel-config`." 89 | ([] 90 | (let [props (System/getProperties) ;;<1> 91 | figwheel-config (fsys/fetch-config) 92 | all-builds (->> figwheel-config :data :all-builds (mapv :id))] 93 | (start-figwheel (keys (select-keys props all-builds))))) 94 | ([build-ids] 95 | (let [figwheel-config (fsys/fetch-config) 96 | target-config (-> figwheel-config 97 | (assoc-in [:data :build-ids] 98 | (or (seq build-ids) ;;<2> 99 | (-> figwheel-config :data :build-ids))))] ;;<2> 100 | (-> (cp/system-map 101 | :css-watcher (fsys/css-watcher {:watch-paths ["resources/public/css"]}) ;;<3> 102 | :figwheel-system (fsys/figwheel-system target-config)) 103 | cp/start :figwheel-system fsys/cljs-repl)))) ;;<4> 104 | ---- 105 | <1> If no arguments, default to looking at the JVM system properties, + 106 | eg: if you have a build with id `:test`, it will look for `-Dtest` 107 | <2> Otherwise it will use the passed in `build-ids`, or the default figwheel ids. 108 | <3> OPTIONAL, starts a css watcher so you get automatic reloading of css when it changes. 109 | <4> Starts a cljs repl you can interact with, see https://github.com/bhauman/lein-figwheel/tree/master/sidecar#starting-the-repl[figwheel sidecar] for more information 110 | 111 | Open `localhost:PORT/untangled-spec-client-tests.html`, where `PORT` is defined (with leiningen) by your figwheel configuration in your link:../project.clj[project.clj], eg: `:figwheel {:server-port PORT}`. 112 | 113 | == CI Tests 114 | 115 | * You should have a top level link:../package.json[package.json] file for installing the following: 116 | 117 | [source,json] 118 | ---- 119 | { 120 | "devDependencies": { 121 | "karma": "^0.13.19", 122 | "karma-chrome-launcher": "^0.2.2", 123 | "karma-firefox-launcher": "^0.1.7", 124 | "karma-cljs-test": "^0.1.0" 125 | } 126 | } 127 | ---- 128 | 129 | * You have https://github.com/bensu/doo#doo[lein-doo] as a plugin for running tests through karma *via* nodejs. 130 | * A link:../test/untangled_spec/all_tests.cljs[all_tests.cljs] file for running your tests. 131 | 132 | [source] 133 | ---- 134 | (ns your.all-tests 135 | (:require 136 | untangled-spec.tests-to-run ;; <1> 137 | [doo.runner :refer-macros [doo-all-tests]])) 138 | 139 | (doo-all-tests #"untangled-spec\..*-spec") ;; <2> 140 | ---- 141 | <1> For loading your test namespaces. 142 | <2> Takes a regex to run just your test namespaces. 143 | 144 | //SEPARATOR - NEEDED WHY? 145 | * A `:doo` section to configure the CI runner. Chrome is the recommended target js-env. 146 | 147 | [source] 148 | ---- 149 | :doo {:build "automated-tests", :paths {:karma "node_modules/karma/bin/karma"}} 150 | ---- 151 | 152 | * A cljsbuild with id `:automated-tests` is the CI tests output. 153 | 154 | [source] 155 | ---- 156 | {:source-paths ["src" "test"] 157 | :compiler {:output-to "resources/private/js/unit-tests.js" 158 | :main untangled-spec.all-tests 159 | :optimizations :none}} 160 | ---- 161 | 162 | * An html file in your `resources/private/`, eg: link:../resources/private/unit-tests.html[unit-tests.html], for renderering your `automated-tests` build. 163 | 164 | See https://github.com/bensu/doo#usage[lein-doo usage] for up to date details on how to use it from the command line and how to setup the all_tests like file, + 165 | TLDR: `lein doo ${js-env} automated-tests once`, where for ex `${js-env}` is `chrome`. 166 | 167 | == Server Tests 168 | 169 | === With reporting in the terminal 170 | 171 | * The https://github.com/jakemcc/lein-test-refresh[lein test-refresh plugin], which will re-run server tests on save, and can be configured (see the `:test-refresh` section in the link:../project.clj[project.clj]). 172 | * link:../dev/clj/user.clj[user.clj] : The entry point for running clojure tests that should be rendered in the browser. 173 | 174 | Use `lein test-refresh` at the command line. 175 | Read https://github.com/jakemcc/lein-test-refresh/blob/master/CHANGES.md#040[this changelog entry] 176 | for information on using test-selectors. + 177 | 178 | A recommended sample configuration is: 179 | 180 | [source] 181 | ---- 182 | :test-refresh {:report untangled-spec.reporters.terminal/untangled-report <1> 183 | :changes-only true <2> 184 | :with-repl true} <3> 185 | ---- 186 | <1> REQUIRED, `:report` must point to the correct report function. 187 | <2> Only re-runs tests that have changed, useful if you have slow and/or many tests. 188 | <3> Gives you a limited, but handy, repl! 189 | 190 | For up to date and comprehensive information, you should treat 191 | https://github.com/jakemcc/lein-test-refresh[lein-test-refresh] 192 | itself as the authoritative source. 193 | 194 | === With reporting in the browser 195 | 196 | * Create a webserver using `untangled-spec.suite/def-test-suite` that runs your tests and talks with your browser. + 197 | See the docstring for further info on the arguments. 198 | 199 | [source] 200 | ---- 201 | (:require 202 | [untangled-spec.suite :as suite]) 203 | 204 | (suite/def-test-suite my-test-suite <1> 205 | {:config {:port 8888} <2> 206 | :test-paths ["test"] <3><4> 207 | :source-paths ["src"]} <4> 208 | {:available #{:focused :unit :integration} <5> 209 | :default #{:untangled-spec.selectors/none :focused :unit}}) <6> 210 | ---- 211 | <1> Defines a function you can call to (re-)start the test-suite, should be useful if you are changing the following arguments, ie: config, paths, or selectors. 212 | <2> `:config` is passed directly to the webserver, only port is required and currently advertised as available. 213 | <3> `:test-paths` is about finding your test namespaces. 214 | <4> `:source-paths` is concatenated with `:test-paths` to create a set of paths that the test-suite will watch for any changes, and refresh and namespaces contain therein. 215 | <5> Is where you define your `:available` selectors, ie: legal/possible values for selectors on your `specification`. 216 | <6> `:default` selectors to use when you haven't yet specified any in the browser ui. 217 | 218 | //SEPARATOR - NEEDED WHY? 219 | * Call `my-test-suite` and go to `localhost:PORT/untangled-spec-server-tests.html` to view your test report. 220 | 221 | == Anatomy of a specification 222 | 223 | The main testing macros are `specification`, `behavior`, `component`, and `assertions`: 224 | 225 | [source] 226 | ---- 227 | (:require 228 | [untangled-spec.core :refer [specification behavior component assertions]) 229 | 230 | (specification "A Thing" 231 | (component "A Thing Part" 232 | (behavior "does something" 233 | (assertions 234 | form => expected-result 235 | form2 => expected-result2 236 | 237 | "optional sub behavior clause" 238 | form3 => expected-result3))) 239 | ---- 240 | 241 | See the clojure.spec/def for `::assertions` in link:../src/untangled_spec/assertions.cljc[assertions.cljc] for the grammar of the `assertions` macro. 242 | 243 | [NOTE] 244 | ==== 245 | `component` is an alias of `behavior`. + 246 | It can read better if you are describing a *component* footnote:[ 247 | *Noun*: a part or element of a larger whole. 248 | *Adjective*: constituting part of a larger whole; constituent. 249 | ] and not a behavior footnote:[*Noun*: the way in which a natural phenomenon or a machine works or functions.]. 250 | ==== 251 | 252 | [TIP] 253 | ==== 254 | `specification` =outputs=> `(clojure|cljs).test/deftest`, + 255 | `behavior` =outputs=> `(clojure|cljs).test/testing`. 256 | 257 | You are therefore free to use any functions from https://clojure.github.io/clojure/clojure.test-api.html[clojure.test] or https://github.com/clojure/clojurescript/wiki/Testing[cljs.test] inside their body. 258 | 259 | However, we recommend you use these macros as opposed to `deftest` and `testing` as they emit extra reporting events that are used by our renderers. + 260 | You are however ok to use `is` instead of `assertions` if you prefer it. 261 | ==== 262 | 263 | === Assertions 264 | 265 | Assertions provides some explict arrows, unlike https://github.com/marick/Midje[Midje] which uses black magic, for use in making your tests more concise and readable. 266 | 267 | [source] 268 | ---- 269 | (:require 270 | [untangled-spec.core :refer [assertions]) 271 | 272 | (assertions 273 | actual => expected ;;<1> 274 | actual =fn=> (fn [act] ... ok?) ;;<2> 275 | actual =throws=> ExceptionType ;; <3><6> 276 | actual =throws=> (ExceptionType opt-regex opt-pred) ;;<4><6> 277 | actual =throws=> {:ex-type opt-ex-type :regex opt-regex :fn opt-pred}) ;; <5><6> 278 | ---- 279 | <1> Checks that actual is equal to expected, either can be anything. 280 | <2> `expected` is a function takes `actual` and returns a truthy value. 281 | <3> Expects that actual will throw an Exception and checks that the type is `ExceptionType`. 282 | <4> Can also optionally that the message matches the `opt-regex` & `opt-pred`. 283 | <5> An alternative supported syntax is a map with all optional keys `:ex-type` `:regex` `:fn` 284 | <6> View the clojure.spec/def `::criteria` link:../src/untangled_spec/assertions.cljc[assertions.cljc] for the up to date grammar for the `expected` side of a `=throws=>` assertions. 285 | 286 | === Mocking 287 | 288 | The mocking system does a lot in a very small space. It can be invoked via the `provided` or `when-mocking` macro. 289 | The former requires a string and adds an outline section. The latter does not change the outline output. 290 | The idea with `provided` is that you are stating an assumption about some way other parts of the system are behaving for that test. 291 | 292 | Mocking must be done in the context of a specification, and creates a scope for all sub-outlines. Generally 293 | you want to isolate mocking to a specific behavior: 294 | 295 | [source] 296 | ---- 297 | (:require 298 | [untangled-spec.core :refer [specification behavior when-mocking assertions]) 299 | 300 | ;; source file 301 | (defn my-function [x y] (launch-rockets!)) 302 | ;; spec file 303 | (specification "Thing" 304 | (behavior "Does something" 305 | (when-mocking 306 | (my-function arg1 arg2) 307 | => (do (assertions 308 | arg1 => 3 309 | arg2 => 5) 310 | true) 311 | ;;actual test 312 | (assertions 313 | (my-function 3 5) => true)))) 314 | ---- 315 | 316 | Basically, you include triples (a form, arrow, form), followed by the code & tests to execute. 317 | 318 | It is important to note that the mocking support does a bunch of verification at the end of your test: 319 | 320 | . It uses the mocked functions in the order specified. 321 | . It verifies that your functions are called the appropriate number of times (at least once is the default) and no more if a number is specified. 322 | . It captures the arguments in the symbols you provide (in this case arg1 and arg2). These are available for use in the RHS of the mock expression. 323 | . If the mocked function has a `clojure.spec/fdef` with `:args`, it will validate the arguments with it. 324 | . It returns whatever the RHS of the mock expression indicates. 325 | . If the mocked function has a `clojure.spec/fdef` with `:ret`, it will validate the return value with it. 326 | . If the mocked function has a `clojure.spec/fdef` with `:fn` (and `:args` & `:ret`), it will validate the arguments and return value with it. 327 | . If assertions run in the RHS form, they will be honored (for test failures). 328 | 329 | So, the following mock script should pass: 330 | 331 | [source] 332 | ---- 333 | (:require 334 | [untangled-spec.core :refer [when-mocking assertions]) 335 | 336 | (when-mocking 337 | (f a) =1x=> a ;;<1> 338 | (f a) =2x=> (+ 1 a) ;;<2> 339 | (g a b) => 17 ;;<3> 340 | 341 | (assertions 342 | (+ (f 2) (f 2) (f 2) 343 | (g 3e6 :foo/bar) 344 | (g "otherwise" :invalid)) <4> 345 | => 42)) 346 | ---- 347 | 348 | <1> The first call to `f` returns the argument. 349 | <2> The next two calls return the argument plus one. 350 | <3> `g` can be called any amount (but at least once) and returns 17 each time. 351 | <4> If you were to remove any call to `f` or `g` this test would fail. 352 | 353 | ==== Clojure.spec mocking integration 354 | 355 | However, the following mock script will fail due to clojure.spec errors: 356 | 357 | [source] 358 | ---- 359 | (:require 360 | [clojure.spec :as s] 361 | [untangled-spec.core :refer [when-mocking assertions]) 362 | 363 | (s/fdef f 364 | :args number? 365 | :ret number? 366 | :fn #(< (:args %) (:ret %))) 367 | (defn f [a] (+ a 42)) 368 | 369 | (when-mocking 370 | (f "asdf") =1x=> 123 ;; <1> 371 | (f a) =1x=> :fdsa ;; <2> 372 | (f a) =1x=> (- 1 a) ;; <3> 373 | 374 | (assertions 375 | (+ (f "asdf") (f 1) (f 2)) => 42)) 376 | ---- 377 | <1> Fails the `:args` spec `number?` 378 | <2> Fails the `:ret` spec `number?` 379 | <3> Fails the `:fn` spec `(< args ret)` 380 | 381 | ==== Spies 382 | 383 | Sometimes it is desirable to check that a function is called but still use its original definition, this pattern is called a test spy. 384 | Here's an example of how to do that with untangled spec: 385 | 386 | [source] 387 | ---- 388 | (:require 389 | [untangled-spec.core :refer [when-mocking assertions]) 390 | 391 | (let [real-fn f] 392 | (when-mocking f => (do ... (real-fn)) 393 | (assertions 394 | ...) 395 | ---- 396 | 397 | ==== Protocols and Inline functions 398 | 399 | When working with protocols and records, or inline functions (eg: https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/core.clj#L965[+]), it is useful to be able to mock them just as a regular function. 400 | The fix for doing so is quite straightforward: 401 | [source] 402 | ---- 403 | ;; source file 404 | (defprotocol MockMe 405 | (-please [this f x] ...)) ;;<1> 406 | (defn please [this f x] (-please this f x)) ;;<2> 407 | 408 | (defn fn-under-test [this] 409 | ... (please this inc :counter) ...) ;;<3> 410 | 411 | ;; test file 412 | (:require 413 | [untangled-spec.core :refer [when-mocking assertions]) 414 | 415 | (when-mocking 416 | (please this f x) => (do ...) ;;<4> 417 | (assertions 418 | (fn-under-test ...) => ...))) ;;<5> 419 | ---- 420 | <1> define the protocol & method 421 | <2> define a function that just calls the protocol 422 | <3> use the wrapper function instead of the protocol 423 | <4> mock the wrapping function from (2) 424 | <5> keep calm and carry on testing 425 | 426 | === Timeline testing 427 | 428 | On occasion you'd like to mock things that use callbacks. Chains of callbacks can be a challenge to test, especially 429 | when you're trying to simulate timing issues. 430 | 431 | [source] 432 | ---- 433 | (:require 434 | [cljs.test :refer [is]] 435 | [untangled-spec.core :refer [specification provided with-timeline 436 | tick async]]) 437 | 438 | (def a (atom 0)) 439 | 440 | (specification "Some Thing" 441 | (with-timeline 442 | (provided "things happen in order" 443 | (js/setTimeout f tm) =2x=> (async tm (f)) 444 | 445 | (js/setTimeout 446 | (fn [] 447 | (reset! a 1) 448 | (js/setTimeout 449 | (fn [] (reset! a 2)) 200)) 100) 450 | 451 | (tick 100) 452 | (is (= 1 @a)) 453 | 454 | (tick 100) 455 | (is (= 1 @a)) 456 | 457 | (tick 100) 458 | (is (= 2 @a)))) 459 | ---- 460 | 461 | In the above scripted test the `provided` (when-mocking with a label) is used to mock out `js/setTimeout`. By 462 | wrapping that provided in a `with-timeline` we gain the ability to use the `async` and `tick` macros (which must be 463 | pulled in as macros in the namespace). The former can be used on the RHS of a mock to indicate that the actual 464 | behavior should happen some number of milliseconds in the *simulated* future. 465 | 466 | So, this test says that when `setTimeout` is called we should simulate waiting however long that 467 | call requested, then we should run the captured function. Note that the `async` macro doesn't take a symbol to 468 | run, it instead wants you to supply a full form to run (so you can add in arguments, etc). 469 | 470 | Next this test does a nested `setTimeout`! This is perfectly fine. Calling the `tick` function advances the 471 | simulated clock. So, you can see we can watch the atom change over \"time\"! 472 | 473 | Note that you can schedule multiple things, and still return a value from the mock! 474 | 475 | [source] 476 | ---- 477 | (:require 478 | [untangled-spec.core :refer [provided with-timeline async]]) 479 | 480 | (with-timeline 481 | (when-mocking 482 | (f a) => (do (async 200 (g)) (async 300 (h)) true))) 483 | ---- 484 | 485 | the above indicates that when `f` is called it will schedule `(g)` to run 200ms from \"now\" and `(h)` to run 486 | 300ms from \"now\". Then `f` will return `true`. 487 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "untangled-spec", 3 | "version": "1.0.0", 4 | "description": "Testing", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "doc", 8 | "test": "test" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "karma": "^0.13.19", 13 | "karma-cljs-test": "^0.1.0", 14 | "karma-phantomjs-launcher": "^1.0.4" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@gitlab.buehner-fry.com:navis/untangled-spec.git" 19 | }, 20 | "author": "", 21 | "license": "NAVIS" 22 | } 23 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject navis/untangled-spec "1.0.0-alpha4-SNAPSHOT" 2 | :description "A Behavioral specification system for clj and cljs stacked on clojure.test" 3 | :url "" 4 | :license {:name "MIT Public License" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[colorize "0.1.1" :exclusions [org.clojure/clojure]] 7 | [com.lucasbradstreet/cljs-uuid-utils "1.0.2"] 8 | [com.taoensso/timbre "4.8.0"] 9 | [kibu/pushy "0.3.6"] 10 | [lein-doo "0.1.6" :scope "test"] 11 | [navis/untangled-client "0.8.0"] 12 | [navis/untangled-server "0.7.0" :exclusions [com.taoensso/timbre org.clojure/java.classpath]] 13 | [navis/untangled-ui "1.0.0-alpha1"] 14 | [navis/untangled-websockets "0.3.3"] 15 | [org.clojure/clojure "1.9.0-alpha14"] 16 | [org.clojure/clojurescript "1.9.473"] 17 | [org.clojure/tools.namespace "0.3.0-alpha3"] 18 | [org.omcljs/om "1.0.0-alpha48"]] 19 | 20 | :plugins [[com.jakemccrary/lein-test-refresh "0.19.0" :exclusions [org.clojure/tools.namespace]] 21 | [lein-cljsbuild "1.1.5"] 22 | [lein-doo "0.1.6"] ;; for cljs CI tests 23 | [lein-shell "0.5.0"]] 24 | 25 | :release-tasks [["shell" "bin/release" "all_tasks"]] 26 | 27 | :source-paths ["src"] 28 | :test-paths ["test"] 29 | :resource-paths ["resources"] 30 | 31 | ;; this for backwards compatability, should now use untangled-spec.suite/def-test-suite 32 | ;; (see dev/clj/user.clj for an example) 33 | :test-refresh {:report untangled-spec.reporters.terminal/untangled-report 34 | :changes-only true 35 | :with-repl true} 36 | :test-selectors {:default (complement :should-fail)} 37 | 38 | ;; CI tests: Set up to support karma runner. Recommend running against chrome. See README 39 | :doo {:build "automated-tests" 40 | :paths {:karma "node_modules/karma/bin/karma"}} 41 | 42 | :clean-targets ^{:protect false} [:target-path "target" "resources/public/js" "resources/private/js"] 43 | 44 | :cljsbuild {:builds {;; For rendering specs without figwheel (eg: server side tests) 45 | :spec-renderer {:source-paths ["src"] 46 | :compiler {:main untangled-spec.spec-renderer 47 | :output-to "resources/public/js/test/untangled-spec-renderer.js" 48 | :output-dir "resources/public/js/test/untangled-spec-renderer" 49 | :asset-path "js/test/untangled-spec-renderer" 50 | :optimizations :simple}}}} 51 | 52 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 53 | 54 | :figwheel {:nrepl-port 7888 55 | :server-port 3457} 56 | 57 | :aliases {"jar" ["with-profile" "with-cljs" "jar"] 58 | "test-cljs" ["with-profile" "test" "doo" "phantom" "automated-tests" "once"] 59 | "test-clj" ["test-refresh" ":run-once"]} 60 | 61 | :profiles {:with-cljs {:prep-tasks ["compile" ["cljsbuild" "once" "spec-renderer"]]} 62 | :test {:cljsbuild {:builds {:automated-tests {:doc "For CI tests. Runs via doo" 63 | :source-paths ["src" "test"] 64 | :compiler {:output-to "resources/private/js/unit-tests.js" 65 | :output-dir "resources/private/js/unit-tests" 66 | :asset-path "js/unit-tests" 67 | :main untangled-spec.all-tests 68 | :optimizations :simple}}}}} 69 | :dev {:cljsbuild {:builds {:test {:source-paths ["src" "dev" "test"] 70 | :figwheel {:on-jsload cljs.user/on-load} 71 | :compiler {:main cljs.user 72 | :output-to "resources/public/js/test/test.js" 73 | :output-dir "resources/public/js/test/out" 74 | :asset-path "js/test/out" 75 | :optimizations :none}}}} 76 | :source-paths ["src" "test" "dev"] 77 | :repl-options {:init-ns clj.user 78 | :port 7007 79 | :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 80 | :dependencies [[com.cemerick/piggieback "0.2.1"] 81 | [figwheel-sidecar "0.5.8" :exclusions [ring/ring-core http-kit joda-time]] 82 | [org.clojure/tools.nrepl "0.2.12"] 83 | [org.clojure/test.check "0.9.0"]]}}) 84 | -------------------------------------------------------------------------------- /resources/private/unit-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is just a dummy HTML file with which to load the unit tests. 4 | This file could be changed to include HTML for the tests to use 5 | during their operation. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/public/css/untangled-spec-styles.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | 5 | .test-item { 6 | display: block; 7 | font-size: 13pt; 8 | } 9 | 10 | .test-namespace { 11 | margin-top: 20px; 12 | font-weight: 400; 13 | display: block; 14 | font-size: 18pt; 15 | } 16 | 17 | .foldable a { 18 | cursor: pointer; 19 | } 20 | 21 | .test-header { 22 | font-weight: 700; 23 | display: block; 24 | font-size: 18pt; 25 | } 26 | 27 | .test-manual { 28 | color: orange; 29 | } 30 | 31 | .test-pass { 32 | color: limegreen; 33 | } 34 | 35 | .test-error { 36 | color: red; 37 | } 38 | 39 | .test-fail { 40 | color: red; 41 | } 42 | 43 | .test-list { 44 | list-style-type: none; 45 | } 46 | 47 | .test-result { 48 | margin: 12px; 49 | font-size: 16px; 50 | } 51 | 52 | .test-result-title { 53 | width: 100px; 54 | font-size: 16px; 55 | font-weight: bold; 56 | } 57 | 58 | .test-report ul { 59 | padding-left: 10px; 60 | margin-bottom: 10px; 61 | } 62 | 63 | .test-report ul:empty { 64 | display: none; 65 | } 66 | 67 | .test-report h2 { 68 | font-size: 24px; 69 | margin-bottom: 15px; 70 | } 71 | 72 | /* EDN RENDERING CSS (use in diffs) */ 73 | 74 | .rendered-edn .collection { 75 | display: flex; 76 | display: -webkit-flex; 77 | } 78 | 79 | .rendered-edn .keyval { 80 | display: flex; 81 | display: -webkit-flex; 82 | flex-wrap: wrap; 83 | -webkit-flex-wrap: wrap; 84 | } 85 | 86 | .rendered-edn .keyval > .keyword { 87 | color: #a94442; 88 | } 89 | 90 | .rendered-edn .keyval > *:first-child { 91 | margin: 0px 3px; 92 | flex-shrink: 0; 93 | -webkit-flex-shrink: 0; 94 | } 95 | 96 | .rendered-edn .keyval > *:last-child { 97 | margin: 0px 3px; 98 | } 99 | 100 | .rendered-edn .opener { 101 | color: #999; 102 | margin: 0px 4px; 103 | flex-shrink: 0; 104 | -webkit-flex-shrink: 0; 105 | } 106 | 107 | .rendered-edn .closer { 108 | display: flex; 109 | display: -webkit-flex; 110 | flex-direction: column-reverse; 111 | -webkit-flex-direction: column-reverse; 112 | margin: 0px 3px; 113 | color: #999; 114 | } 115 | 116 | .rendered-edn .string { 117 | color: #428bca; 118 | } 119 | 120 | .rendered-edn .string .opener, 121 | .rendered-edn .string .closer { 122 | display: inline; 123 | margin: 0px; 124 | color: #428bca; 125 | } 126 | 127 | .rendered-edn .highlight { 128 | background-color: black; 129 | color: white; 130 | } 131 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/public/untangled-spec-client-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Loading "js/test/test.js", if you need to name that something else (conflicts?) make your own test html file
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/public/untangled-spec-server-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/untangled_spec/assertions.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.assertions 2 | #?(:cljs 3 | (:require-macros 4 | [untangled-spec.assertions :refer [define-assert-exprs!]])) 5 | (:require 6 | #?(:clj [clojure.test]) 7 | cljs.test ;; contains multimethod in clojure file 8 | [clojure.spec :as s] 9 | #?(:clj [untangled-spec.impl.macros :as im]) 10 | [untangled-spec.spec :as us])) 11 | 12 | (s/def ::arrow (comp #{"=>" "=fn=>" "=throws=>"} str)) 13 | (s/def ::behavior string?) 14 | (s/def ::triple (s/cat 15 | :actual ::us/any 16 | :arrow ::arrow 17 | :expected ::us/any)) 18 | (s/def ::block (s/cat 19 | :behavior (s/? ::behavior) 20 | :triples (s/+ ::triple))) 21 | (s/def ::assertions (s/+ ::block)) 22 | 23 | (defn fn-assert-expr [msg [f arg :as form]] 24 | `(let [arg# ~arg 25 | result# (~f arg#)] 26 | {:type (if result# :pass :fail) 27 | :message ~msg :assert-type '~'exec 28 | :actual arg# :expected '~f})) 29 | 30 | (defn eq-assert-expr [msg [exp act :as form]] 31 | `(let [act# ~act 32 | exp# ~exp 33 | result# (im/try-report ~msg (= exp# act#))] 34 | {:type (if result# :pass :fail) 35 | :message ~msg :assert-type '~'eq 36 | :actual act# :expected exp#})) 37 | 38 | (defn parse-criteria [[tag x]] 39 | (case tag :sym {:ex-type x} x)) 40 | 41 | (defn check-error* [msg e & [ex-type regex fn fn-pr]] 42 | (let [e-msg (or #?(:clj (.getMessage e) :cljs (.-message e)) (str e))] 43 | (->> (cond 44 | (some-> (ex-data e) :type (= ::internal)) 45 | {:type :error :extra e-msg 46 | :actual e :expected "it to throw"} 47 | 48 | (and ex-type (not= ex-type (type e))) 49 | {:type :fail :actual (type e) :expected ex-type 50 | :extra "exception did not match type"} 51 | 52 | (and regex (not (re-find regex e-msg))) 53 | {:type :fail :actual e-msg :expected (str regex) 54 | :extra "exception's message did not match regex"} 55 | 56 | (and fn (not (fn e))) 57 | {:type :fail :actual e :expected fn-pr 58 | :extra "checker function failed"} 59 | 60 | :else {:type :pass :actual "act" :expected "exp"}) 61 | (merge {:message msg 62 | :assert-type 'throws? 63 | :throwable e})))) 64 | 65 | (defn check-error [msg e criteria & [fn-pr]] 66 | (apply check-error* msg e 67 | ((juxt :ex-type :regex :fn :fn-pr) 68 | (assoc criteria :fn-pr fn-pr)))) 69 | 70 | (s/def ::ex-type symbol?) 71 | (s/def ::regex ::us/regex) 72 | (s/def ::fn ::us/any) 73 | (s/def ::criteria 74 | (s/or 75 | :sym symbol? 76 | :list (s/cat :ex-type ::ex-type :regex (s/? ::regex) :fn (s/? ::fn)) 77 | :map (s/keys :opt-un [::ex-type ::regex ::fn]))) 78 | 79 | (defn throws-assert-expr [msg [cljs? should-throw criteria]] 80 | (let [criteria (parse-criteria (us/conform! ::criteria criteria))] 81 | `(try ~should-throw 82 | (throw (ex-info "Expected an error to be thrown!" 83 | {:type ::internal :criteria ~criteria})) 84 | (catch ~(if (not cljs?) (symbol "Throwable") (symbol "js" "Object")) 85 | e# (check-error ~msg e# ~criteria))))) 86 | 87 | (defn assert-expr [msg [disp-key & form]] 88 | (case disp-key 89 | = (eq-assert-expr msg form) 90 | exec (fn-assert-expr msg form) 91 | throws? (throws-assert-expr msg form) 92 | {:type :fail :message msg :actual disp-key 93 | :expected #{"exec" "eq" "throws?"}})) 94 | 95 | (defn triple->assertion [cljs? {:keys [actual arrow expected]}] 96 | (let [prefix (if cljs? "cljs.test" "clojure.test") 97 | is (symbol prefix "is") 98 | msg (str actual " " arrow " " expected)] 99 | (case arrow 100 | => 101 | `(~is (~'= ~expected ~actual) 102 | ~msg) 103 | 104 | =fn=> 105 | (let [checker expected 106 | arg actual] 107 | `(~is (~'exec ~checker ~arg) 108 | ~msg)) 109 | 110 | =throws=> 111 | (let [should-throw actual 112 | criteria expected] 113 | `(~is (~'throws? ~cljs? ~should-throw ~criteria) 114 | ~msg)) 115 | 116 | (throw (ex-info "invalid arrow" {:arrow arrow}))))) 117 | 118 | (defn fix-conform [conformed-assertions] 119 | ;;see issue: #31 120 | (if (vector? (second conformed-assertions)) 121 | (vec (cons (first conformed-assertions) (second conformed-assertions))) 122 | conformed-assertions)) 123 | 124 | (defn block->asserts [cljs? {:keys [behavior triples]}] 125 | (let [asserts (map (partial triple->assertion cljs?) triples)] 126 | `(im/with-reporting ~(when behavior {:type :behavior :string behavior}) 127 | ~@asserts))) 128 | 129 | #?(:clj 130 | (defmacro define-assert-exprs! [] 131 | (let [test-ns (im/if-cljs &env "cljs.test" "clojure.test") 132 | do-report (symbol test-ns "do-report") 133 | t-assert-expr (im/if-cljs &env cljs.test/assert-expr clojure.test/assert-expr) 134 | do-assert-expr 135 | (fn [args] 136 | (let [[msg form] (cond-> args (im/cljs-env? &env) rest)] 137 | `(~do-report ~(assert-expr msg form))))] 138 | (defmethod t-assert-expr '= eq-ae [& args] (do-assert-expr args)) 139 | (defmethod t-assert-expr 'exec fn-ae [& args] (do-assert-expr args)) 140 | (defmethod t-assert-expr 'throws? throws-ae [& args] (do-assert-expr args)) 141 | nil))) 142 | (define-assert-exprs!) 143 | -------------------------------------------------------------------------------- /src/untangled_spec/async.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.async 2 | (:require 3 | [#?(:cljs cljs.pprint :clj clojure.pprint) :refer [pprint]])) 4 | 5 | (defprotocol IAsyncQueue 6 | (current-time [this] "Returns the current time on the simulated clock, in ms") 7 | (peek-event [this] "Returns the first event on the queue") 8 | (advance-clock [this ms] 9 | "Move the clock forward by the specified number of ms, triggering events (even those added by interstitial triggers) in the correct order up to (and including) events that coincide with the final time.") 10 | (schedule-event [this ms-from-now fn-to-call] 11 | "Schedule an event which should occur at some time in the future (offset from now).")) 12 | 13 | (defrecord Event [abs-time fn-to-call]) 14 | 15 | (defn process-first-event! 16 | "Triggers the first event in the queue (runs it), and removes it from the queue." 17 | [queue] 18 | (if-let [evt (peek-event queue)] 19 | (do 20 | ((:fn-to-call evt)) 21 | (swap! (:schedule queue) #(dissoc % (:abs-time evt)))))) 22 | 23 | (defrecord AsyncQueue [schedule now] 24 | IAsyncQueue 25 | (current-time [this] @(:now this)) 26 | (peek-event [this] (second (first @(-> this :schedule)))) 27 | (advance-clock 28 | [this ms] 29 | (let [stop-time (+ ms @(:now this))] 30 | (loop [evt (peek-event this)] 31 | (let [now (or (:abs-time evt) (inc stop-time))] 32 | (if (<= now stop-time) 33 | (do 34 | (reset! (:now this) now) 35 | (process-first-event! this) 36 | (recur (peek-event this)))))) 37 | (reset! (:now this) stop-time))) 38 | (schedule-event 39 | [this ms-from-now fn-to-call] 40 | (let [tm (+ ms-from-now @(:now this)) 41 | event (Event. tm fn-to-call)] 42 | (if (contains? @(:schedule this) tm) 43 | (throw (ex-info (str "Schedule already contains an event " ms-from-now "ms from 'now' which would generate an indeterminant ordering for your events. Please offset your submission time a bit") {})) 44 | (swap! (:schedule this) #(assoc % (:abs-time event) event)))))) 45 | 46 | (defn make-async-queue 47 | "Build an asynchronous event simulation queue." 48 | [] 49 | (AsyncQueue. (atom (sorted-map)) (atom 0))) 50 | -------------------------------------------------------------------------------- /src/untangled_spec/contains.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.contains 2 | (:require 3 | [clojure.set :refer [subset?]] 4 | [untangled-spec.spec :as us])) 5 | 6 | (defn todo [] 7 | (throw (ex-info "todo / not-implemented" {}))) 8 | 9 | (defn ->kw-type [x] 10 | (cond 11 | (string? x) :string (us/regex? x) :regex 12 | (map? x) :map (set? x) :set 13 | (list? x) :list (vector? x) :list)) 14 | 15 | (defmulti -contains? 16 | (fn [& xs] (->> xs drop-last (mapv ->kw-type)))) 17 | 18 | (defmethod -contains? [:string :string] [act exp & [opt]] 19 | (re-find (re-pattern exp) act)) 20 | 21 | (defmethod -contains? [:string :regex] [act exp & [opt]] 22 | (re-find exp act)) 23 | 24 | (defmethod -contains? [:map :map] [act exp & [opt]] 25 | (subset? (set exp) (set act))) 26 | 27 | (defmethod -contains? [:map :list] [act exp & [opt]] 28 | (case opt;cond-> ? 29 | :keys (subset? (set exp) (set (keys act))) 30 | :vals (subset? (set exp) (set (vals act))) 31 | false)) 32 | 33 | (defmethod -contains? [:set :set] [act exp & [opt]] ;1+ 34 | (some exp act)) 35 | (defmethod -contains? [:set :list] [act exp & [opt]] ;all 36 | (subset? (set exp) act)) 37 | 38 | (defmethod -contains? [:list :set] [act exp & [opt]] ;1+ 39 | (some exp act)) 40 | (defmethod -contains? [:list :list] [act exp & [opt]] ;all 41 | (case opt 42 | :gaps 43 | (todo) 44 | 45 | :any-order 46 | (todo) 47 | 48 | :both 49 | (todo) 50 | 51 | (let [pad (fn [p s] (str p s p)) 52 | strip (fn [s] (->> s str drop-last rest (apply str))) 53 | re (->> exp strip (pad " "))] 54 | (re-find (re-pattern re) (->> act strip (pad " ")))))) 55 | 56 | (defmethod -contains? :default [exp act] (todo)) 57 | 58 | (defn *contains? 59 | ([exp] (fn [act] (*contains? act exp nil))) 60 | ([exp opt] (fn [act] (*contains? act exp opt))) 61 | ([act exp opt] (-contains? act exp opt))) 62 | -------------------------------------------------------------------------------- /src/untangled_spec/core.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.core 2 | (:require 3 | [clojure.spec :as s] 4 | [clojure.string :as str] 5 | [clojure.test] 6 | [untangled-spec.assertions :as ae] 7 | [untangled-spec.async :as async] 8 | [untangled-spec.impl.macros :as im] 9 | [untangled-spec.provided :as p] 10 | [untangled-spec.runner] ;;side effects 11 | [untangled-spec.selectors :as sel] 12 | [untangled-spec.stub] 13 | [untangled-spec.spec :as us])) 14 | 15 | (defn var-name-from-string [s] 16 | (symbol (str "__" (str/replace s #"[^\w\d\-\!\#\$\%\&\*\_\<\>\:\?\|]" "-") "__"))) 17 | 18 | (s/def ::specification 19 | (s/cat 20 | :name string? 21 | :selectors (s/* keyword?) 22 | :body (s/* ::us/any))) 23 | 24 | (s/fdef specification :args ::specification) 25 | (defmacro specification 26 | "Defines a specification which is translated into a what a deftest macro produces with report hooks for the 27 | description. Technically outputs a deftest with additional output reporting. 28 | When *load-tests* is false, the specification is ignored." 29 | [& args] 30 | (let [{:keys [name selectors body]} (us/conform! ::specification args) 31 | test-name (-> (var-name-from-string name) 32 | (str (gensym)) symbol 33 | (with-meta (zipmap selectors (repeat true)))) 34 | prefix (im/if-cljs &env "cljs.test" "clojure.test")] 35 | `(~(symbol prefix "deftest") ~test-name 36 | (im/when-selected-for ~(us/conform! ::sel/test-selectors selectors) 37 | (im/with-reporting {:type :specification :string ~name} 38 | (im/try-report ~name 39 | ~@body)))))) 40 | 41 | (s/def ::behavior (s/cat 42 | :name (constantly true) 43 | :opts (s/* keyword?) 44 | :body (s/* ::us/any))) 45 | (s/fdef behavior :args ::behavior) 46 | 47 | (defmacro behavior 48 | "Adds a new string to the list of testing contexts. May be nested, 49 | but must occur inside a specification. If the behavior is not machine 50 | testable then include the keyword :manual-test just after the behavior name 51 | instead of code. 52 | 53 | (behavior \"blows up when the moon is full\" :manual-test)" 54 | [& args] 55 | (let [{:keys [name opts body]} (us/conform! ::behavior args) 56 | typekw (if (contains? opts :manual-test) 57 | :manual :behavior) 58 | prefix (im/if-cljs &env "cljs.test" "clojure.test")] 59 | `(~(symbol prefix "testing") ~name 60 | (im/with-reporting ~{:type typekw :string name} 61 | (im/try-report ~name 62 | ~@body))))) 63 | 64 | (defmacro component 65 | "An alias for behavior. Makes some specification code easier to read where a given specification is describing subcomponents of a whole." 66 | [& args] `(behavior ~@args)) 67 | 68 | (defmacro provided 69 | "A macro for using a Midje-style provided clause within any testing framework. 70 | This macro rewrites assertion-style mocking statements into code that can do that mocking. 71 | See the clojure.spec for `::p/mocks`. 72 | See the doc string for `p/parse-arrow-count`." 73 | [string & forms] 74 | (p/provided* (im/cljs-env? &env) string forms)) 75 | 76 | (defmacro when-mocking 77 | "A macro that works just like 'provided', but requires no string and outputs no extra text in the test output. 78 | See the clojure.spec for `::p/mocks`. 79 | See the doc string for `p/parse-arrow-count`." 80 | [& forms] 81 | (p/provided* (im/cljs-env? &env) :skip-output forms)) 82 | 83 | (s/fdef assertions :args ::ae/assertions) 84 | (defmacro assertions [& forms] 85 | (let [blocks (ae/fix-conform (us/conform! ::ae/assertions forms)) 86 | asserts (map (partial ae/block->asserts (im/cljs-env? &env)) blocks)] 87 | `(do ~@asserts true))) 88 | 89 | (defmacro with-timeline 90 | "Adds the infrastructure required for doing timeline testing" 91 | [& forms] 92 | `(let [~'*async-queue* (async/make-async-queue)] 93 | ~@forms)) 94 | 95 | (defmacro async 96 | "Adds an event to the event queue with the specified time and callback function. 97 | Must be wrapped by with-timeline. 98 | " 99 | [tm cb] 100 | `(async/schedule-event ~'*async-queue* ~tm (fn [] ~cb))) 101 | 102 | (defmacro tick 103 | "Advances the timer by the specified number of ticks. 104 | Must be wrapped by with-timeline." 105 | [tm] 106 | `(async/advance-clock ~'*async-queue* ~tm)) 107 | -------------------------------------------------------------------------------- /src/untangled_spec/core.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.core 2 | (:require-macros 3 | [untangled-spec.core]) 4 | (:require 5 | [cljs.test :include-macros true] 6 | [untangled-spec.assertions] 7 | [untangled-spec.async] 8 | [untangled-spec.runner] ;;side effects 9 | [untangled-spec.selectors] 10 | [untangled-spec.stub])) 11 | -------------------------------------------------------------------------------- /src/untangled_spec/diff.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.diff 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.walk :as walk])) 5 | 6 | (declare diff) 7 | (def nf '..nothing..) 8 | 9 | (defn diff-elem? [?de] 10 | (and (vector? ?de) 11 | (= 4 (count ?de)) 12 | (let [[p _ m _] ?de] 13 | (and (= p :+) (= m :-))))) 14 | 15 | (defn diff-elem 16 | ([] []) 17 | ([exp got] 18 | (assert (not (diff-elem? exp))) 19 | (assert (not (diff-elem? got))) 20 | [:+ exp :- got])) 21 | 22 | (defn extract [?d] 23 | (assert (and (vector? ?d) (= 2 (count ?d)))) 24 | (assert (vector? (first ?d))) 25 | (assert (diff-elem? (second ?d))) 26 | (let [[path [_ exp _ got]] ?d] 27 | {:path path :exp exp :got got})) 28 | 29 | (defn diff? [?d] 30 | (and ?d (map? ?d) 31 | (every? vector (keys ?d)) 32 | (every? diff-elem? (vals ?d)))) 33 | 34 | (defn map-keys [f m] (into {} (for [[k v] m] [(f k) v]))) 35 | 36 | (defn- map-diff [ks exp act] 37 | (loop [[k & ks] ks, exp exp, act act, path [], diffs {}] 38 | (if (and (empty? ks) (nil? k)) diffs 39 | (let [ev (get exp k nf) 40 | av (get act k nf)] 41 | (if (and ev av (= ev av)) 42 | (recur ks exp act path diffs) 43 | (let [d (diff ev av :recur) 44 | diffs (cond 45 | (diff-elem? d) (assoc diffs [k] d) 46 | (diff? d) (merge diffs (map-keys #(vec (cons k %)) d)) 47 | (empty? d) diffs 48 | :else (throw (ex-info "This should not have happened" 49 | {:d d :exp exp :act act})))] 50 | (recur ks exp act [] diffs))))))) 51 | 52 | (defn- seq-diff [exp act] 53 | (let [exp-count (count exp) 54 | act-count (count act)] 55 | (loop [[i & is] (range), [e & es :as exp] exp, [a & as :as act] act, diffs {}] 56 | (cond 57 | (and (seq exp) (seq act) (not= e a)) 58 | (let [d (diff e a :recur) 59 | diffs (cond 60 | (diff-elem? d) 61 | (assoc diffs [i] d) 62 | (diff? d) (map-keys #(vec (cons i %)) d) 63 | (empty? d) diffs 64 | :else (throw (ex-info "This should not have happened" 65 | {:d d :exp exp :act act})))] 66 | (recur is es as diffs)) 67 | 68 | (and (seq exp) (>= i act-count)) 69 | (recur is es as (assoc diffs [i] (diff-elem e nf))) 70 | 71 | (and (>= i exp-count) (seq act)) 72 | (recur is es as (assoc diffs [i] (diff-elem nf a))) 73 | 74 | (and (>= i exp-count) (>= i act-count)) diffs 75 | 76 | :else (recur is es as diffs))))) 77 | 78 | (defn set-diff [exp act] 79 | (let [missing-from-act (set/difference act exp) 80 | missing-from-exp (set/difference exp act)] 81 | (if (or (seq missing-from-act) (seq missing-from-exp)) 82 | (diff-elem missing-from-exp missing-from-act) 83 | (diff-elem)))) 84 | 85 | (defn diff [exp act & [opt]] 86 | (let [recur? (#{:recur} opt)] 87 | (cond-> 88 | (cond 89 | (every? map? [exp act]) 90 | (map-diff (vec (set (mapcat keys [exp act]))) 91 | exp act) 92 | 93 | (every? string? [exp act]) 94 | (diff-elem exp act) 95 | 96 | (every? set? [exp act]) 97 | (cond->> (set-diff exp act) 98 | (not recur?) (assoc {} [])) 99 | 100 | (every? sequential? [exp act]) 101 | (seq-diff exp act) 102 | 103 | (not= (type exp) (type act)) 104 | (diff-elem exp act) 105 | 106 | (every? coll? [exp act]) 107 | (seq-diff exp act) 108 | 109 | ;; RECUR GUARD 110 | (not recur?) {} 111 | 112 | (not= exp act) 113 | (diff-elem exp act) 114 | 115 | :else []) 116 | (not recur?) (#(cond->> % (diff-elem? %) (assoc {} [])))))) 117 | 118 | (defn patch 119 | ([x diffs] 120 | (patch x diffs 121 | {:get-exp 122 | ;; so we dont get noisy (& useless) diffs 123 | (fn not-a-nf-diff [d] 124 | (let [{:keys [exp]} (extract d)] 125 | (when-not (= nf exp) exp)))})) 126 | ([x diffs {:keys [get-exp] 127 | :or {get-exp (comp :exp extract)}}] 128 | (let [list->lvec #(cond-> % (seq? %) (-> vec (with-meta {::list true}))) 129 | lvec->list #(cond-> % (and (vector? %) (-> % meta ::list true?)) vec)] 130 | ;; we turn lists into vectors and back so that we can assoc-in on them 131 | (as-> x <> 132 | (walk/prewalk list->lvec <>) 133 | (reduce (fn [x d] 134 | (let [path (seq (-> d extract :path)) 135 | exp (get-exp d)] 136 | (cond 137 | (not path) {[] exp} ;; empty path, top level diff 138 | exp (assoc-in x path exp) 139 | (and (nil? exp) (not (map? x))) (assoc-in x path nil) 140 | ;; else drop the missing item from up a level 141 | :else (if-let [path' (seq (drop-last path))] 142 | (update-in x path' dissoc (last path)) 143 | (dissoc x (last path)))))) 144 | <> diffs) 145 | (walk/prewalk lvec->list <>))))) 146 | 147 | (defn compress [[x & _ :as coll]] 148 | (let [diff* (partial apply diff)] 149 | (->> coll 150 | (partition 2 1) 151 | (map (comp diff* reverse)) 152 | (cons x) 153 | (into (empty coll))))) 154 | 155 | (defn decompress [[x & xs :as coll]] 156 | (->> (reductions patch x xs) 157 | (into (empty coll)))) 158 | -------------------------------------------------------------------------------- /src/untangled_spec/dom/edn_renderer.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.dom.edn-renderer 2 | (:require 3 | [cljs.pprint :refer [pprint]] 4 | [untangled-spec.diff :as diff] 5 | [om.dom :as dom]) 6 | (:import 7 | (goog.string StringBuffer))) 8 | 9 | (defonce ^:dynamic *key-counter* nil) 10 | 11 | (defn get-key [] 12 | (swap! *key-counter* inc) 13 | (str "k-" @*key-counter*)) 14 | 15 | (declare html) 16 | 17 | (defn literal? [x] 18 | (and (not (seq? x)) 19 | (not (coll? x)))) 20 | 21 | (defn separator* [s] 22 | (dom/div #js {:className "separator" 23 | :key (get-key)} 24 | s)) 25 | 26 | (defn clearfix-separator* [s] 27 | (dom/span #js {:key (get-key)} 28 | (separator* s) 29 | (dom/span #js {:className "clearfix"}))) 30 | 31 | (defn separate-fn [coll] 32 | (if (not (every? literal? coll)) clearfix-separator* separator*)) 33 | 34 | (defn interpose-separator [rct-coll s sep-fn] 35 | (->> (rest rct-coll) 36 | (interleave (repeatedly #(sep-fn s))) 37 | (cons (first rct-coll)) 38 | to-array)) 39 | 40 | (defn pprint-str [obj] 41 | (let [sb (StringBuffer.)] 42 | (pprint obj (StringBufferWriter. sb)) 43 | (str sb))) 44 | 45 | (defn literal [class x] 46 | (dom/span #js {:className class :key (get-key)} 47 | (pprint-str x))) 48 | 49 | (defn join-html [separator coll] 50 | (interpose-separator (mapv html coll) 51 | separator 52 | (separate-fn coll))) 53 | 54 | (defn html-keyval [[k v]] 55 | (dom/span #js {:className "keyval" 56 | :key (prn-str k)} 57 | (html k) 58 | (html v))) 59 | 60 | (defn html-keyvals [coll] 61 | (interpose-separator (mapv html-keyval coll) 62 | " " 63 | (separate-fn (vals coll)))) 64 | 65 | (defn open-close [class-str opener closer rct-coll] 66 | (dom/span #js {:className class-str :key (str (hash rct-coll))} 67 | (dom/span #js {:className "opener" :key 1} opener) 68 | (dom/span #js {:className "contents" :key 2} rct-coll) 69 | (dom/span #js {:className "closer" :key 3} closer))) 70 | 71 | (defn html-collection [class opener closer coll] 72 | (open-close (str "collection " class ) opener closer (join-html " " coll))) 73 | 74 | (defn html-map [coll] 75 | (open-close "collection map" "{" "}" (html-keyvals coll))) 76 | 77 | (defn html-string [s] 78 | (open-close "string" "\"" "\"" s)) 79 | 80 | (defn html [x] 81 | (cond 82 | (number? x) (literal "number" x) 83 | (keyword? x) (literal "keyword" x) 84 | (symbol? x) (literal "symbol" x) 85 | (string? x) (html-string x) 86 | (map? x) (html-map x) 87 | (set? x) (html-collection "set" "#{" "}" x) 88 | (vector? x) (html-collection "vector" "[" "]" x) 89 | (seq? x) (html-collection "seq" "(" ")" x) 90 | :else (literal "literal" x))) 91 | 92 | (defn html-edn [e & [diff]] 93 | (binding [*key-counter* (atom 0)] 94 | (dom/div #js {:className "rendered-edn com-rigsomelight-devcards-typog"} 95 | (try 96 | (html (cond-> e diff (diff/patch diff))) 97 | (catch js/Object e (html "DIFF CRASHED ON OUTPUT")))))) 98 | -------------------------------------------------------------------------------- /src/untangled_spec/impl/macros.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.impl.macros 2 | (:require 3 | [untangled-spec.selectors :as sel])) 4 | 5 | (defn cljs-env? 6 | "https://github.com/Prismatic/schema/blob/master/src/clj/schema/macros.clj" 7 | [env] (boolean (:ns env))) 8 | 9 | (defn if-cljs [env cljs clj] 10 | (if (cljs-env? env) cljs clj)) 11 | 12 | (defmacro try-report [block & body] 13 | (let [prefix (if-cljs &env "cljs.test" "clojure.test") 14 | do-report (symbol prefix "do-report")] 15 | `(try ~@body 16 | (catch ~(if-cljs &env (symbol "js" "Object") (symbol "Throwable")) 17 | e# (~do-report {:type :error :actual e# 18 | :message ~block :expected "IT TO NOT THROW!"}))))) 19 | 20 | (defmacro with-reporting 21 | "Wraps body in a begin-* and an end-* do-report if the msg contains a :type" 22 | [msg & body] 23 | (let [cljs? (cljs-env? &env) 24 | do-report (symbol (if cljs? "cljs.test" "clojure.test") "do-report") 25 | make-msg (fn [msg-loc] 26 | (update msg :type 27 | #(keyword (str msg-loc "-" (name %)))))] 28 | (if-not (:type msg) `(do ~@body) 29 | `(do 30 | (~do-report ~(make-msg "begin")) 31 | ~@body 32 | (~do-report ~(make-msg "end")))))) 33 | 34 | (defmacro when-selected-for 35 | "Only runs body if it is selectors for based on the passed in selectors. 36 | See untangled-spec.selectors for more info." 37 | [selectors & body] 38 | `(when (sel/selected-for? ~selectors) 39 | ~@body)) 40 | -------------------------------------------------------------------------------- /src/untangled_spec/impl/runner.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.impl.runner 2 | (:require 3 | [clojure.tools.namespace.repl :as tools-ns-repl] 4 | [untangled-spec.runner :as runner])) 5 | 6 | (tools-ns-repl/disable-reload!) 7 | 8 | (def runner (atom {})) 9 | -------------------------------------------------------------------------------- /src/untangled_spec/impl/selectors.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load untangled-spec.impl.selectors 2 | (:require 3 | #?(:clj [clojure.tools.namespace.repl :as tools-ns-repl]))) 4 | 5 | #?(:clj (tools-ns-repl/disable-reload!)) 6 | 7 | (defonce selectors (atom {:current nil :default nil})) 8 | -------------------------------------------------------------------------------- /src/untangled_spec/provided.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.provided 2 | (:require 3 | [clojure.spec :as s] 4 | [clojure.string :as str] 5 | [untangled-spec.impl.macros :as im] 6 | [untangled-spec.stub :as stub] 7 | [untangled-spec.spec :as us])) 8 | 9 | (defn parse-arrow-count 10 | "parses how many times the mock/stub should be called with. 11 | * => implies 1+, 12 | * =Ax=> implies exactly A times. 13 | Provided arrow counts cannot be zero because mocking should 14 | not be a negative assertion, but a positive one. 15 | IE: verify that what you want to be called is called instead, 16 | or that, if necessary, it should throw an error if called." 17 | [sym] 18 | (let [nm (name sym) 19 | number (re-find #"\d+" nm) 20 | not-zero-msg "Arrow count must not be zero in provided clauses."] 21 | (assert (re-find #"^=" nm) "Arrows must start with = (try =#x=>)") 22 | (assert (re-find #"=>$" nm) "Arrows must end with => (try =#x=>)") 23 | (cond 24 | (= "=>" nm) :many 25 | (= "0" number) (assert false not-zero-msg) 26 | number (Integer/parseInt number)))) 27 | 28 | (defn symbol->any [s] 29 | (cond 30 | (= '& s) ::stub/&_ 31 | (symbol? s) ::stub/any 32 | :else s)) 33 | 34 | (defn literal->gensym [l] 35 | (if (symbol? l) l (gensym "arg"))) 36 | 37 | (defn parse-mock-triple [{:as triple :keys [under-mock arrow result]}] 38 | (merge under-mock 39 | (let [{:keys [params]} under-mock 40 | arglist (map literal->gensym params)] 41 | {:ntimes (parse-arrow-count arrow) 42 | :literals (mapv symbol->any params) 43 | :stub-function `(fn [~@arglist] ~result) }))) 44 | 45 | (defn parse-mocks [mocks] 46 | (let [parse-steps 47 | (fn parse-steps [[mock-name steps :as group]] 48 | (let [symgen (gensym "script")] 49 | {:script `(stub/make-script ~(name mock-name) 50 | ~(mapv (fn make-step [{:keys [stub-function ntimes literals]}] 51 | `(stub/make-step ~stub-function ~ntimes ~literals)) 52 | steps)) 53 | :sstub `(stub/scripted-stub ~symgen) 54 | :mock-name mock-name 55 | :symgen symgen}))] 56 | (->> mocks 57 | (map parse-mock-triple) 58 | (group-by :mock-name) 59 | (map parse-steps)))) 60 | 61 | (defn arrow? [sym] 62 | (and (symbol? sym) 63 | (re-find #"^=" (name sym)) 64 | (re-find #"=>$" (name sym)))) 65 | 66 | (s/def ::under-mock 67 | (s/and list? 68 | (s/cat 69 | :mock-name symbol? 70 | :params (s/* ::us/any)))) 71 | (s/def ::arrow arrow?) 72 | (s/def ::triple 73 | (s/cat 74 | :under-mock ::under-mock 75 | :arrow ::arrow 76 | :result ::us/any)) 77 | (s/def ::mocks 78 | (s/cat 79 | :mocks (s/+ ::triple) 80 | :body (s/+ ::us/any))) 81 | 82 | (defn provided* 83 | [cljs? string forms] 84 | (let [{:keys [mocks body]} (us/conform! ::mocks forms) 85 | scripts (parse-mocks mocks) 86 | skip-output? (= :skip-output string)] 87 | `(im/with-reporting ~(when-not skip-output? {:type :provided :string (str "PROVIDED: " string)}) 88 | (let [~@(mapcat (juxt :symgen :script) scripts)] 89 | (with-redefs [~@(mapcat (juxt :mock-name :sstub) scripts)] 90 | ~@body 91 | (stub/validate-target-function-counts ~(mapv :symgen scripts))))))) 92 | -------------------------------------------------------------------------------- /src/untangled_spec/renderer.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.renderer 2 | (:require 3 | [clojure.string :as str] 4 | [cognitect.transit :as transit] 5 | [goog.string :as gstr] 6 | [com.stuartsierra.component :as cp] 7 | [goog.dom :as gdom] 8 | [om.dom :as dom] 9 | [om.next :as om :refer-macros [defui]] 10 | [pushy.core :as pushy] 11 | [untangled.client.core :as uc] 12 | [untangled.client.data-fetch :as df] 13 | [untangled.client.impl.network :as un] 14 | [untangled.client.mutations :as m] 15 | [untangled-spec.dom.edn-renderer :refer [html-edn]] 16 | [untangled-spec.diff :as diff] 17 | [untangled-spec.selectors :as sel] 18 | [untangled.icons :as ui.i] 19 | [untangled.ui.layout :as ui.l] 20 | [untangled.ui.elements :as ui.e] 21 | [untangled.websockets.networking :as wn]) 22 | (:import 23 | (goog.date DateTime) 24 | (goog.i18n DateTimeFormat))) 25 | 26 | (enable-console-print!) 27 | 28 | (defn test-item-class [{:keys [fail error pass manual]}] 29 | (str "test-" 30 | (cond 31 | (pos? fail) "fail" 32 | (pos? error) "error" 33 | (pos? pass) "pass" 34 | (pos? manual) "manual" 35 | :else "pending"))) 36 | 37 | (defn color-favicon-data-url [color] 38 | (let [cvs (.createElement js/document "canvas")] 39 | (set! (.-width cvs) 16) 40 | (set! (.-height cvs) 16) 41 | (let [ctx (.getContext cvs "2d")] 42 | (set! (.-fillStyle ctx) color) 43 | (.fillRect ctx 0 0 16 16)) 44 | (.toDataURL cvs))) 45 | 46 | (defn change-favicon-to-color [color] 47 | (let [icon (.getElementById js/document "favicon")] 48 | (set! (.-href icon) (color-favicon-data-url color)))) 49 | 50 | (defn has-status? [p] 51 | (fn has-status?* [x] 52 | (or (p (:status x)) 53 | (and 54 | (seq (:test-items x)) 55 | (seq (filter has-status?* (:test-items x))))))) 56 | 57 | (def filters 58 | (let [report-as (fn [status] #(update % :status select-keys [status])) 59 | no-test-results #(dissoc % :test-results)] 60 | {:all (map identity) 61 | :failing (filter (comp #(some pos? %) (juxt :fail :error) :status)) 62 | :manual (comp (filter (has-status? #(-> % :manual pos?))) 63 | (map no-test-results) 64 | (map (report-as :manual))) 65 | :passing (comp (filter (comp pos? :pass :status)) 66 | (map (report-as :pass))) 67 | :pending (comp (filter (has-status? #(->> % vals (apply +) zero?))) 68 | (map no-test-results) 69 | (map (report-as :pending)))})) 70 | 71 | (defui ^:once Foldable 72 | Object 73 | (initLocalState [this] {:folded? true}) 74 | (render [this] 75 | (let [{:keys [folded?]} (om/get-state this) 76 | {:keys [render]} (om/props this) 77 | {:keys [title value classes]} (render folded?)] 78 | (dom/div #js {:className "foldable"} 79 | (dom/a #js {:className classes 80 | :onClick #(om/update-state! this update :folded? not)} 81 | (if folded? \u25BA \u25BC) 82 | (if folded? 83 | (str (apply str (take 40 title)) 84 | (when (< 40 (count title)) "...")) 85 | (str title))) 86 | (dom/div #js {:className (when folded? "hidden")} 87 | value))))) 88 | (def ui-foldable (om/factory Foldable {:keyfn #(gensym "foldable")})) 89 | 90 | (defui ^:once ResultLine 91 | Object 92 | (render [this] 93 | (let [{:keys [title value stack type]} (om/props this)] 94 | (dom/tr nil 95 | (dom/td #js {:className (str "test-result-title " 96 | (name type))} 97 | title) 98 | (dom/td #js {:className "test-result"} 99 | (dom/code nil 100 | (ui-foldable 101 | {:render (fn [folded?] 102 | {:title (if stack (str value) 103 | (if folded? (str value) title)) 104 | :value (if stack stack (if-not folded? (html-edn value))) 105 | :classes (if stack "stack")})}))))))) 106 | (def ui-result-line (om/factory ResultLine {:keyfn #(gensym "result-line")})) 107 | 108 | (defui ^:once HumanDiffLines 109 | Object 110 | (render [this] 111 | (let [d (om/props this) 112 | {:keys [exp got path]} (diff/extract d)] 113 | (when (seq path) 114 | (dom/table #js {:className "human-diff-lines"} 115 | (dom/tbody nil 116 | (dom/tr #js {:className "path"} 117 | (dom/td nil "at: ") 118 | (dom/td nil (str path))) 119 | (dom/tr #js {:className "expected"} 120 | (dom/td nil "exp: ") 121 | (dom/td nil (html-edn exp))) 122 | (dom/tr #js {:className "actual"} 123 | (dom/td nil "got: ") 124 | (dom/td nil (html-edn got))))))))) 125 | (def ui-human-diff-lines (om/factory HumanDiffLines {:keyfn #(gensym "human-diff-lines")})) 126 | 127 | (defui ^:once HumanDiff 128 | Object 129 | (render [this] 130 | (let [{:keys [diff actual]} (om/props this) 131 | [fst rst] (split-at 2 diff)] 132 | (->> (dom/div nil 133 | (mapv ui-human-diff-lines fst) 134 | (when (seq rst) 135 | (ui-foldable {:render 136 | (fn [folded?] 137 | {:title "& more" 138 | :value (mapv ui-human-diff-lines rst) 139 | :classes ""})}))) 140 | (dom/td nil) 141 | (dom/tr nil 142 | (dom/td nil "DIFFS:")))))) 143 | (def ui-human-diff (om/factory HumanDiff {:keyfn #(gensym "human-diff")})) 144 | 145 | (defui ^:once TestResult 146 | Object 147 | (render [this] 148 | (let [{:keys [where message extra actual expected stack diff]} (om/props this)] 149 | (->> (dom/tbody nil 150 | (dom/tr nil 151 | (dom/td #js {:className "test-result-title"} 152 | "Where: ") 153 | (dom/td #js {:className "test-result"} 154 | (str/replace (str where) 155 | #"G__\d+" ""))) 156 | (when message 157 | (ui-result-line {:type :normal 158 | :title "ASSERTION: " 159 | :value message})) 160 | (ui-result-line {:type :normal 161 | :title "Actual: " 162 | :value actual 163 | :stack stack}) 164 | (ui-result-line {:type :normal 165 | :title "Expected: " 166 | :value expected}) 167 | (when extra 168 | (ui-result-line {:type :normal 169 | :title "Message: " 170 | :value extra})) 171 | (when-let [diff (and diff (seq (filter (comp seq key) diff)))] 172 | (ui-human-diff {:actual actual 173 | :diff diff}))) 174 | (dom/table nil) 175 | (dom/li nil))))) 176 | (def ui-test-result (om/factory TestResult {:keyfn :id})) 177 | 178 | (declare ui-test-item) 179 | 180 | (defui ^:once TestItem 181 | Object 182 | (render [this] 183 | (let [{:keys [current-filter] :as test-item-data} (om/props this)] 184 | (dom/li #js {:className "test-item"} 185 | (dom/div nil 186 | (dom/span #js {:className (test-item-class (:status test-item-data))} 187 | (:name test-item-data)) 188 | (dom/ul #js {:className "test-list"} 189 | (mapv ui-test-result 190 | (:test-results test-item-data))) 191 | (dom/ul #js {:className "test-list"} 192 | (sequence 193 | (comp (filters current-filter) 194 | (map #(assoc % :current-filter current-filter)) 195 | (map ui-test-item)) 196 | (:test-items test-item-data)))))))) 197 | (def ui-test-item (om/factory TestItem {:keyfn :id})) 198 | 199 | (defui ^:once TestNamespace 200 | Object 201 | (render 202 | [this] 203 | (let [{:keys [current-filter] :as tests-by-namespace} (om/props this)] 204 | (when (seq (:test-items tests-by-namespace)) 205 | (dom/li #js {:className "test-item"} 206 | (dom/div #js {:className "test-namespace"} 207 | (dom/h2 #js {:className (test-item-class (:status tests-by-namespace))} 208 | (str (:name tests-by-namespace))) 209 | (dom/ul #js {:className "test-list"} 210 | (sequence (comp (filters current-filter) 211 | (map #(assoc % :current-filter current-filter)) 212 | (map ui-test-item)) 213 | (:test-items tests-by-namespace))))))))) 214 | (def ui-test-namespace (om/factory TestNamespace {:keyfn :name})) 215 | 216 | (defn filter-button 217 | ([icon data] 218 | (filter-button icon data (gensym) (gensym) (constantly nil))) 219 | ([icon data this-filter current-filter toggle-filter-cb] 220 | (let [is-active? (= this-filter current-filter)] 221 | (dom/button #js {:className "c-button c-button--icon" 222 | :onClick (toggle-filter-cb this-filter)} 223 | (ui.i/icon icon :states (cond-> [] is-active? (conj :active))) 224 | (dom/span #js {:className (cond-> "c-message" is-active? 225 | (str " c-message--primary"))} 226 | data))))) 227 | 228 | (defn find-tests [test-filter namespaces] 229 | (remove 230 | (some-fn nil? (comp seq :test-items)) 231 | (apply tree-seq 232 | #_branch? (comp seq :test-items) 233 | #_children (comp (partial sequence (filters test-filter)) :test-items) 234 | (sequence (filters test-filter) namespaces)))) 235 | 236 | (defn test-info [{:keys [pass fail error namespaces end-time run-time]} 237 | current-filter toggle-filter-cb] 238 | (let [total (+ pass fail error) 239 | end-time (.format (new DateTimeFormat "HH:mm:ss") 240 | (or (and end-time (.setTime (new DateTime) end-time)) 241 | (new DateTime))) 242 | run-time (str/replace-first 243 | (gstr/format "%.3fs" 244 | (float (/ run-time 1000))) 245 | #"^0" "")] 246 | (if (pos? (+ fail error)) 247 | (change-favicon-to-color "#d00") 248 | (change-favicon-to-color "#0d0")) 249 | (dom/span nil 250 | (filter-button :assignment (count namespaces)) 251 | (filter-button :help total) 252 | (filter-button :check pass 253 | :passing current-filter toggle-filter-cb) 254 | (filter-button :update (count (find-tests :pending namespaces)) 255 | :pending current-filter toggle-filter-cb) 256 | (filter-button :pan_tool (count (find-tests :manual namespaces)) 257 | :manual current-filter toggle-filter-cb) 258 | (filter-button :close fail 259 | :failing current-filter toggle-filter-cb) 260 | (filter-button :warning error 261 | :failing current-filter toggle-filter-cb) 262 | (filter-button :access_time end-time) 263 | (filter-button :hourglass_empty run-time)))) 264 | 265 | (defui ^:once SelectorControl 266 | static om/IQuery 267 | (query [this] [:selector/id :selector/active?]) 268 | Object 269 | (render [this] 270 | (let [{:keys [selector/id selector/active?]} (om/props this)] 271 | (dom/div #js {:className "c-drawer__action" :key (str id)} 272 | (ui.e/ui-checkbox 273 | {:id (str id) 274 | :checked active? 275 | :onChange (fn [e] 276 | (om/transact! this 277 | `[(sel/set-selector 278 | ~{:selector/id id 279 | :selector/active? (.. e -target -checked)})]))}) 280 | (dom/span #js {} (str id)))))) 281 | (def ui-selector-control (om/factory SelectorControl {:keyfn :selector/id})) 282 | 283 | (defn test-selectors [selectors] 284 | (dom/div nil 285 | (dom/h1 nil "Test Selectors:") 286 | (map ui-selector-control 287 | (sort-by :selector/id selectors)))) 288 | 289 | (defn toolbar-button [toggle-drawer] 290 | (dom/div #js {:className "c-toolbar__button"} 291 | (dom/button #js {:className "c-button c-button--icon" 292 | :onClick toggle-drawer} 293 | (ui.i/icon :menu)))) 294 | 295 | (defn test-header [test-report current-filter toggle-drawer toggle-filter-cb] 296 | (dom/header #js {:className "u-layout__header c-toolbar c-toolbar--raised"} 297 | (dom/div #js {:className "c-toolbar__row"} 298 | (dom/h1 nil "Untangled Spec") 299 | (dom/div #js {:className "c-toolbar__spacer"}) 300 | (test-info test-report current-filter toggle-filter-cb)) 301 | (toolbar-button toggle-drawer))) 302 | 303 | (defn test-main [{:keys [namespaces]} current-filter] 304 | (dom/main #js {:className "u-layout__content"} 305 | (dom/article #js {:className "o-article"} 306 | (ui.l/row {} 307 | (ui.l/col {:width 12} 308 | (dom/div #js {:className "test-report"} 309 | (dom/ul nil 310 | (sequence 311 | (comp 312 | (filters current-filter) 313 | (map #(assoc % :current-filter current-filter)) 314 | (map ui-test-namespace)) 315 | (sort-by :name namespaces))))))))) 316 | 317 | (defui ^:once TestReport 318 | static uc/InitialAppState 319 | (initial-state [this _] {:ui/react-key (gensym "UI_REACT_KEY") 320 | :ui/current-filter :all}) 321 | static om/IQuery 322 | (query [this] [:ui/react-key :test-report :ui/current-filter {:selectors (om/get-query SelectorControl)}]) 323 | Object 324 | (render [this] 325 | (let [{:keys [ui/react-key test-report selectors ui/current-filter] :as props} (om/props this) 326 | {:keys [open-drawer?]} (om/get-state this) 327 | toggle-drawer #(om/update-state! this update :open-drawer? not) 328 | toggle-filter-cb (fn [f] #(om/transact! this `[(toggle-filter ~{:filter f})]))] 329 | (dom/div #js {:key react-key :className "u-layout"} 330 | (dom/div #js {:className "u-layout__page u-layout__page--fixed"} 331 | (test-header test-report current-filter toggle-drawer toggle-filter-cb) 332 | (dom/div #js {:className (cond-> "c-drawer" open-drawer? (str " is-open"))} 333 | (dom/div #js {:className "c-drawer__header"} 334 | (dom/img #js {:src "img/logo.png" :height 35 :width 35 335 | :onClick toggle-drawer}) 336 | (dom/h1 nil "Untangled Spec")) 337 | (test-selectors selectors)) 338 | (dom/div #js {:className (cond-> "c-backdrop" open-drawer? (str " is-active")) 339 | :onClick toggle-drawer}) 340 | (test-main test-report current-filter)))))) 341 | 342 | (defmethod m/mutate `render-tests [{:keys [state]} _ new-report] 343 | {:action #(swap! state assoc :test-report new-report)}) 344 | (defmethod wn/push-received `render-tests 345 | [{:keys [reconciler]} {test-report :msg}] 346 | (om/transact! (om/app-root reconciler) 347 | `[(render-tests ~test-report)])) 348 | 349 | (defrecord TestRenderer [root target with-websockets? runner-atom] 350 | cp/Lifecycle 351 | (start [this] 352 | (let [app (uc/new-untangled-client 353 | :networking (if with-websockets? 354 | (wn/make-channel-client "/_untangled_spec_chsk") 355 | (reify un/UntangledNetwork 356 | (start [this app] this) 357 | (send [this edn ok err] 358 | (ok ((om/parser @runner-atom) @runner-atom edn))))) 359 | :started-callback 360 | (fn [app] 361 | (df/load app :selectors SelectorControl 362 | {:post-mutation `sel/set-selectors})))] 363 | (assoc this :app (uc/mount app root target)))) 364 | (stop [this] 365 | (assoc this :app nil))) 366 | 367 | (defn make-test-renderer [{:keys [with-websockets?] :or {with-websockets? true}}] 368 | (map->TestRenderer 369 | {:with-websockets? with-websockets? 370 | :runner-atom (atom nil) 371 | :root TestReport 372 | :target "untangled-spec-report"})) 373 | -------------------------------------------------------------------------------- /src/untangled_spec/reporter.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.reporter 2 | #?(:cljs (:require-macros [untangled-spec.reporter])) 3 | (:require 4 | #?@(:cljs ([cljs-uuid-utils.core :as uuid] 5 | [cljs.stacktrace :refer [parse-stacktrace]])) 6 | [clojure.set :as set] 7 | [clojure.test :as t] 8 | [com.stuartsierra.component :as cp] 9 | [untangled-spec.diff :refer [diff]]) 10 | #?(:clj 11 | (:import 12 | (java.text SimpleDateFormat) 13 | (java.util Date UUID)) 14 | :cljs 15 | (:import 16 | (goog.date Date)))) 17 | 18 | (defn new-uuid [] 19 | #?(:clj (UUID/randomUUID) 20 | :cljs (uuid/uuid-string (uuid/make-random-uuid)))) 21 | 22 | (defn fix-str [s] 23 | (case s 24 | "" "\"\"" 25 | nil "nil" 26 | s)) 27 | 28 | (defn now-time [] 29 | #?(:clj (System/currentTimeMillis) :cljs (js/Date.now))) 30 | 31 | (defn make-testreport 32 | ([] (make-testreport [])) 33 | ([initial-items] 34 | {:id (new-uuid) 35 | :namespaces [] 36 | :start-time (now-time) 37 | :test 0 :pass 0 38 | :fail 0 :error 0})) 39 | 40 | (defn make-testitem 41 | [test-name] 42 | {:id (new-uuid) 43 | :name test-name 44 | :status {} 45 | :test-items [] 46 | :test-results []}) 47 | 48 | (defn make-manual [test-name] (make-testitem (str test-name " (MANUAL TEST)"))) 49 | 50 | #?(:cljs (defn- stack->trace [st] (parse-stacktrace {} st {} {}))) 51 | 52 | (defn merge-in-diff-results 53 | [{:keys [actual expected assert-type] :as test-result}] 54 | (cond-> test-result (#{'eq} assert-type) 55 | (assoc :diff (diff expected actual)))) 56 | 57 | (defn make-test-result 58 | [status t] 59 | (-> t 60 | (merge {:id (new-uuid) 61 | :status status 62 | :where (t/testing-vars-str t)}) 63 | (merge-in-diff-results) 64 | #?(:cljs (#(if (some-> % :actual .-stack) 65 | (assoc % :stack (-> % :actual .-stack stack->trace)) 66 | %))) 67 | (update :actual fix-str) 68 | (update :expected fix-str))) 69 | 70 | (defn make-tests-by-namespace 71 | [test-name] 72 | {:id (new-uuid) 73 | :name test-name 74 | :test-items [] 75 | :status {}}) 76 | 77 | (defn set-test-result [{:keys [state path]} status] 78 | (let [all-paths (sequence 79 | (comp (take-while seq) (map vec)) 80 | (iterate (partial drop-last 2) @path))] 81 | (swap! state 82 | (fn [state] 83 | (reduce (fn [state path] 84 | (update-in state 85 | (conj path :status status) 86 | (fnil inc 0))) 87 | state all-paths))))) 88 | 89 | (defn begin* [{:keys [state path]} t] 90 | (let [path @path 91 | test-item (make-testitem (:string t)) 92 | test-items-count (count (get-in @state (conj path :test-items)))] 93 | (swap! state assoc-in 94 | (conj path :test-items test-items-count) 95 | test-item) 96 | [test-item test-items-count])) 97 | 98 | (defn get-namespace-location [namespaces nsname] 99 | (let [namespace-index 100 | (first (keep-indexed (fn [idx val] 101 | (when (= (:name val) nsname) 102 | idx)) 103 | namespaces))] 104 | (or namespace-index 105 | (count namespaces)))) 106 | 107 | (defn inc-report-counter [type] 108 | (#?(:clj t/inc-report-counter :cljs t/inc-report-counter!) type)) 109 | 110 | (defn failure* [{:as this :keys [state path]} t failure-type] 111 | (inc-report-counter failure-type) 112 | (let [path @path 113 | {:keys [test-results]} (get-in @state path) 114 | new-result (make-test-result failure-type t)] 115 | (set-test-result this failure-type) 116 | (swap! state update-in (conj path :test-results) 117 | conj new-result) 118 | new-result)) 119 | 120 | (defn error [this t] 121 | (failure* this t :error)) 122 | 123 | (defn fail [this t] 124 | (failure* this t :fail)) 125 | 126 | (defn pass [this t] 127 | (inc-report-counter :pass) 128 | (set-test-result this :pass)) 129 | 130 | (defn push-test-item-path [{:keys [path]} test-item index] 131 | (swap! path conj :test-items index)) 132 | 133 | (defn pop-test-item-path [{:keys [path]}] 134 | (swap! path (comp pop pop))) 135 | 136 | (defn begin-namespace [{:keys [state path]} t] 137 | (let [test-name (ns-name (:ns t)) 138 | namespaces (get-in @state (conj @path :namespaces)) 139 | name-space-location (get-namespace-location namespaces test-name)] 140 | (swap! path conj :namespaces name-space-location) 141 | (swap! state assoc-in @path 142 | (make-tests-by-namespace test-name)))) 143 | 144 | (defn end-namespace [this t] (pop-test-item-path this)) 145 | 146 | (defn begin-specification [this t] 147 | (apply push-test-item-path this 148 | (begin* this t))) 149 | 150 | (defn end-specification [this t] (pop-test-item-path this)) 151 | 152 | (defn begin-behavior [this t] 153 | (apply push-test-item-path this 154 | (begin* this t))) 155 | 156 | (defn end-behavior [this t] (pop-test-item-path this)) 157 | 158 | (defn begin-manual [this t] 159 | (apply push-test-item-path this 160 | (begin* this t))) 161 | 162 | (defn end-manual [this t] 163 | (set-test-result this :manual) 164 | (pop-test-item-path this)) 165 | 166 | (defn begin-provided [this t] 167 | (apply push-test-item-path this 168 | (begin* this t))) 169 | 170 | (defn end-provided [this t] (pop-test-item-path this)) 171 | 172 | (defn summary [{:keys [state]} t] 173 | (let [end-time (now-time) 174 | end-date (.getTime (new Date))] 175 | (swap! state 176 | (fn [{:as st :keys [start-time]}] 177 | (-> st 178 | (assoc :end-time end-date) 179 | (assoc :run-time (- end-time start-time)))))) 180 | (swap! state merge t)) 181 | 182 | (defn reset-test-report! [{:keys [state path]}] 183 | (reset! state (make-testreport)) 184 | (reset! path [])) 185 | 186 | (defrecord TestReporter [state path] 187 | cp/Lifecycle 188 | (start [this] this) 189 | (stop [this] 190 | (reset-test-report! this) 191 | this)) 192 | 193 | (defn make-test-reporter 194 | "Just a shell to contain minimum state necessary for reporting" 195 | [] 196 | (map->TestReporter 197 | {:state (atom (make-testreport)) 198 | :path (atom [])})) 199 | 200 | (defn get-test-report [reporter] 201 | @(:state reporter)) 202 | 203 | (defn untangled-report [{:keys [test/reporter] :as system} on-complete] 204 | (fn [t] 205 | (case (:type t) 206 | :pass (pass reporter t) 207 | :error (error reporter t) 208 | :fail (fail reporter t) 209 | :begin-test-ns (begin-namespace reporter t) 210 | :end-test-ns (end-namespace reporter t) 211 | :begin-specification (begin-specification reporter t) 212 | :end-specification (end-specification reporter t) 213 | :begin-behavior (begin-behavior reporter t) 214 | :end-behavior (end-behavior reporter t) 215 | :begin-manual (begin-manual reporter t) 216 | :end-manual (end-manual reporter t) 217 | :begin-provided (begin-provided reporter t) 218 | :end-provided (end-provided reporter t) 219 | :summary (do (summary reporter t) #?(:clj (on-complete system))) 220 | #?@(:cljs [:end-run-tests (on-complete system)]) 221 | nil))) 222 | 223 | #?(:clj 224 | (defmacro with-untangled-reporting [system on-complete & body] 225 | `(binding [t/report (untangled-report ~system ~on-complete)] ~@body))) 226 | -------------------------------------------------------------------------------- /src/untangled_spec/reporters/console.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.reporters.console 2 | (:require [cljs.test])) 3 | 4 | (enable-console-print!) 5 | 6 | (def ^:dynamic *test-level* (atom 0)) 7 | 8 | (defn space-level [] 9 | (apply str (repeat (* 3 @*test-level*) " "))) 10 | 11 | (defmethod cljs.test/report :console [m]) 12 | 13 | (defmethod cljs.test/report [::console :pass] [m] 14 | (cljs.test/inc-report-counter! :pass)) 15 | 16 | (defmethod cljs.test/report [::console :error] [m] 17 | (cljs.test/inc-report-counter! :error) 18 | (println "\nERROR in" (cljs.test/testing-vars-str m)) 19 | (some-> m :message println) 20 | (println "expected:" (pr-str (:expected m))) 21 | (println " actual:" (pr-str (:actual m))) 22 | (println)) 23 | 24 | (defmethod cljs.test/report [::console :fail] [m] 25 | (cljs.test/inc-report-counter! :fail) 26 | (println "\nFAIL in" (cljs.test/testing-vars-str m)) 27 | (some-> m :message println) 28 | (println "expected:" (pr-str (:expected m))) 29 | (println " actual:" (pr-str (:actual m))) 30 | (println)) 31 | 32 | (defmethod cljs.test/report [::console :begin-test-ns] [m] 33 | (println "\nTesting" (name (:ns m)))) 34 | 35 | (defmethod cljs.test/report [::console :begin-specification] [m] 36 | (reset! *test-level* 0) 37 | (println (space-level) (:string m))) 38 | 39 | (defmethod cljs.test/report [::console :end-specification] [m] 40 | (println) 41 | (reset! *test-level* 0)) 42 | 43 | (defmethod cljs.test/report [::console :begin-behavior] [m] 44 | (swap! *test-level* inc) 45 | (println (space-level) (:string m))) 46 | 47 | (defmethod cljs.test/report [::console :end-behavior] [m] 48 | (swap! *test-level* dec)) 49 | 50 | (defmethod cljs.test/report [::console :begin-manual] [m] 51 | (swap! *test-level* inc) 52 | (println (space-level) (:string m))) 53 | 54 | (defmethod cljs.test/report [::console :end-manual] [m] 55 | (swap! *test-level* dec)) 56 | 57 | (defmethod cljs.test/report [::console :begin-provided] [m] 58 | (swap! *test-level* inc) 59 | (println (space-level) (:string m))) 60 | 61 | (defmethod cljs.test/report [::console :end-provided] [m] 62 | (swap! *test-level* dec)) 63 | 64 | (defmethod cljs.test/report [::console :summary] [m] 65 | (println "\nRan" (:test m) "tests containing" 66 | (+ (:pass m) (:fail m) (:error m)) "assertions.") 67 | (println (:fail m) "failures," (:error m) "errors.")) 68 | -------------------------------------------------------------------------------- /src/untangled_spec/reporters/suite.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.reporters.suite 2 | (:require 3 | [untangled-spec.suite :as ts])) 4 | 5 | (defmacro deftest-all-suite [suite-name regex & [selectors]] 6 | `(ts/def-test-suite ~suite-name ~regex ~(or selectors {}))) 7 | -------------------------------------------------------------------------------- /src/untangled_spec/reporters/terminal.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.reporters.terminal 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.pprint :refer [pprint]] 5 | [clojure.stacktrace :as stack] 6 | [clojure.string :as s] 7 | [clojure.test :as t] 8 | [clojure.walk :as walk] 9 | [colorize.core :as c] 10 | [com.stuartsierra.component :as cp] 11 | [io.aviso.exception :as pretty] 12 | [untangled-spec.diff :as diff] 13 | [untangled-spec.reporter :as base])) 14 | 15 | (def cfg 16 | (atom 17 | (let [COLOR (System/getenv "US_DIFF_HL") 18 | DIFF_MODE (System/getenv "US_DIFF_MODE") 19 | DIFF (System/getenv "US_DIFF") 20 | NUM_DIFFS (System/getenv "US_NUM_DIFFS") 21 | FRAME_LIMIT (System/getenv "US_FRAME_LIMIT") 22 | QUICK_FAIL (System/getenv "US_QUICK_FAIL") 23 | FAIL_ONLY (System/getenv "US_FAIL_ONLY") 24 | PRINT_LEVEL (System/getenv "US_PRINT_LEVEL") 25 | PRINT_LENGTH (System/getenv "US_PRINT_LENGTH")] 26 | {:fail-only? (#{"1" "true"} FAIL_ONLY) 27 | :color? (#{"1" "true"} COLOR) 28 | :diff-hl? (#{"hl" "all"} DIFF_MODE) 29 | :diff-list? (not (#{"hl"} DIFF_MODE)) 30 | :diff? (not (#{"0" "false"} DIFF)) 31 | :frame-limit (edn/read-string (or FRAME_LIMIT "10")) 32 | :num-diffs (edn/read-string (or NUM_DIFFS "1")) 33 | :quick-fail? (not (#{"0" "false"} QUICK_FAIL)) 34 | :*print-level* (edn/read-string (or PRINT_LEVEL "3")) 35 | :*print-length* (edn/read-string (or PRINT_LENGTH "2"))}))) 36 | (defn env [k] (get @cfg k)) 37 | (defn merge-cfg! 38 | "For use in the test-refresh repl to change configuration on the fly. 39 | Single arity will show you the possible keys you can use. 40 | Passing an empty map will show you the current values." 41 | ([] (println "Valid cfg keys: " (set (keys @cfg)))) 42 | ([new-cfg] 43 | (doseq [[k v] new-cfg] 44 | (assert (contains? @cfg k) 45 | (str "Invalid key '" k "', try one of these " (set (keys @cfg))))) 46 | (swap! cfg merge new-cfg))) 47 | 48 | (defn color-str [status & strings] 49 | (let [color? (env :color?) 50 | status->color {:normal (comp c/bold c/yellow) 51 | :diff (comp c/bold c/cyan) 52 | :where (comp c/bold c/white)} 53 | color-fn (or (and color? (status->color status)) 54 | (case status 55 | :diff/impl (fn [[got exp]] 56 | ((comp c/bold c/inverse) 57 | (str exp " != " got))) 58 | nil) 59 | (condp (fn [p x] (pos? (p x 0))) status 60 | :fail c/red 61 | :error c/red 62 | :pass c/green 63 | c/reset))] 64 | (apply color-fn strings))) 65 | 66 | (defn pad [pad n] (apply str (repeat n pad))) 67 | 68 | (defn space-level [level] 69 | (pad " " (* 2 level))) 70 | 71 | (defn print-throwable [e] 72 | (print (pretty/format-exception e {:frame-limit (env :frame-limit)})) 73 | (some-> (.getCause e) print-throwable)) 74 | 75 | (defn pretty-str [s n] 76 | (as-> (with-out-str (pprint s)) s 77 | (clojure.string/split s #"\n") 78 | (apply str (interpose (str "\n" (pad " " (inc (* 2 n)))) s)))) 79 | 80 | (defn print-highligted-diff [diff actual] 81 | (let [process-diff-elem (fn [d] 82 | (let [{:keys [got exp]} (diff/extract d)] 83 | (color-str :diff/impl [got exp]))) 84 | patched-actual (diff/patch actual diff process-diff-elem)] 85 | (println (str \" (color-str :diff/impl ["EXP" "ACT"]) \"\:) 86 | (pretty-str patched-actual 2)))) 87 | 88 | (defn print-diff [diff actual print-fn] 89 | (when (and (seq diff) (env :diff?) (diff/diff? diff)) 90 | (println) 91 | (when (env :diff-list?) 92 | (let [num-diffs (env :num-diffs) 93 | num-diffs (if (number? num-diffs) 94 | num-diffs (count diff))] 95 | (println (color-str :diff "diffs:")) 96 | (doseq [d (take num-diffs diff)] 97 | (let [{:keys [exp got path]} (diff/extract d)] 98 | (when (seq path) 99 | (println (str "- at: " path))) 100 | (println " exp:" (pretty-str exp 6)) 101 | (println " got:" (pretty-str got 3)) 102 | (println))) 103 | (when (< num-diffs (count diff)) 104 | (println "&" (- (count diff) num-diffs) "more...")))) 105 | (when (and (env :diff-hl?) (coll? actual)) 106 | (print-highligted-diff diff actual)))) 107 | 108 | (defn ?ellipses [s] 109 | (binding [*print-level* (env :*print-level*) 110 | *print-length* (env :*print-length*)] 111 | (try (apply str (drop-last (with-out-str (pprint (read-string s))))) 112 | (catch Error _ s)))) 113 | 114 | (defn parse-message [m] 115 | (try (->> (read-string (str "[" m "]")) 116 | (sequence (comp (map str) (map base/fix-str))) 117 | (zipmap [:actual :arrow :expected])) 118 | (catch Error _ {:message m}))) 119 | 120 | (defn print-message [m print-fn] 121 | (print-fn (color-str :normal "ASSERTION:") 122 | (let [{:keys [arrow actual expected message]} (parse-message m)] 123 | (or message 124 | (str (-> actual ?ellipses) 125 | " " arrow 126 | " " (-> expected ?ellipses)))))) 127 | 128 | (defn print-extra [e print-fn] 129 | (print-fn (color-str :normal " extra:") e)) 130 | 131 | (defn print-where [w s print-fn] 132 | (let [status->str {:error "Error" 133 | :fail "Failed"}] 134 | (->> (s/replace w #"G__\d+" "") 135 | (str (status->str s) " in ") 136 | (color-str :where) 137 | print-fn))) 138 | 139 | (defn print-test-result [{:keys [message where status actual 140 | expected extra throwable diff]} 141 | print-fn print-level] 142 | (print-fn) 143 | (some-> where (print-where status print-fn)) 144 | (when (and (= status :error) 145 | (instance? Throwable actual)) 146 | (print-throwable actual)) 147 | (when (and throwable 148 | (not (instance? Throwable actual))) 149 | (print-throwable throwable)) 150 | (some-> message (print-message print-fn)) 151 | (when (or (not diff) (empty? diff) 152 | (not (env :diff?)) 153 | (and (not (env :diff-hl?)) 154 | (not (env :diff-list?)))) 155 | (print-fn " Actual:" (pretty-str actual (+ 5 print-level))) 156 | (print-fn " Expected:" (pretty-str expected (+ 5 print-level)))) 157 | (some-> extra (print-extra print-fn)) 158 | (some-> diff (print-diff actual print-fn)) 159 | (when (env :quick-fail?) 160 | (throw (ex-info "" {::stop? true})))) 161 | 162 | (def when-fail-only-keep-failed 163 | (filter #(if-not (env :fail-only?) true 164 | (or (pos? (:fail (:status %) 0)) 165 | (pos? (:error (:status %) 0)))))) 166 | 167 | (defn print-test-item [test-item print-level] 168 | (t/with-test-out 169 | (println (space-level print-level) 170 | (color-str (:status test-item) 171 | (:name test-item))) 172 | (into [] 173 | (comp (filter (comp #{:fail :error} :status)) 174 | (map #(print-test-result % (->> print-level inc space-level 175 | (partial println)) 176 | (inc print-level)))) 177 | (:test-results test-item)) 178 | (into [] 179 | (comp when-fail-only-keep-failed 180 | (map #(print-test-item % (inc print-level)))) 181 | (:test-items test-item)))) 182 | 183 | (defn print-namespace [make-tests-by-namespace] 184 | (t/with-test-out 185 | (println) 186 | (println (color-str (:status make-tests-by-namespace) 187 | "Testing " (:name make-tests-by-namespace))) 188 | (into [] 189 | (comp when-fail-only-keep-failed 190 | (map #(print-test-item % 1))) 191 | (:test-items make-tests-by-namespace)))) 192 | 193 | (defn print-report-data 194 | "Prints the current report data from the report data state and applies colors based on test results" 195 | [reporter] 196 | (do 197 | (defmethod print-method Throwable [e w] 198 | (print-method (c/red e) w)) 199 | (t/with-test-out 200 | (let [{:keys [namespaces test pass fail error]} (base/get-test-report reporter)] 201 | (println "Running tests for:" (map :name namespaces)) 202 | (try (->> namespaces 203 | (into [] when-fail-only-keep-failed) 204 | (sort-by :name) 205 | (mapv print-namespace)) 206 | (catch Exception e 207 | (when-not (->> e ex-data ::stop?) 208 | (print-throwable e)))) 209 | (println "\nRan" test "tests containing" 210 | (+ pass fail error) "assertions.") 211 | (println fail "failures," error "errors."))) 212 | (remove-method print-method Throwable) 213 | reporter)) 214 | 215 | (def this 216 | (cp/start (base/make-test-reporter))) 217 | 218 | (def untangled-report 219 | (base/untangled-report {:test/reporter this} 220 | (comp base/reset-test-report! print-report-data :test/reporter))) 221 | -------------------------------------------------------------------------------- /src/untangled_spec/router.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.router 2 | (:require 3 | [clojure.set :as set] 4 | [com.stuartsierra.component :as cp] 5 | [om.next :as om] 6 | [pushy.core :as pushy] 7 | [untangled-spec.renderer :as renderer] 8 | [untangled-spec.selectors :as sel] 9 | [untangled.client.mutations :as m]) 10 | (:import 11 | (goog.Uri QueryData))) 12 | 13 | (defn parse-fragment [path] 14 | (let [data (new QueryData path)] 15 | {:filter (some-> (.get data "filter") keyword) 16 | :selectors (some->> (.get data "selectors") sel/parse-selectors)})) 17 | 18 | (defn update-fragment! [history k f & args] 19 | (let [data (new goog.Uri.QueryData (pushy/get-token history))] 20 | (.set data (name k) (apply f (.get data (name k)) args)) 21 | (pushy/set-token! history 22 | ;; so we dont get an ugly escaped url 23 | (.toDecodedString data)))) 24 | 25 | (defn assoc-fragment! [history k v] 26 | (update-fragment! history k (constantly v))) 27 | 28 | (defn new-history [parser tx!] 29 | (let [history (with-redefs [pushy/update-history! 30 | #(doto % 31 | (.setUseFragment true) 32 | (.setPathPrefix "") 33 | (.setEnabled true))] 34 | (pushy/pushy tx! parser))] 35 | history)) 36 | 37 | (defmethod m/mutate `set-page-filter [{:keys [state ast]} k {:keys [filter]}] 38 | {:action #(swap! state assoc :ui/current-filter 39 | (or (and (nil? filter) :all) 40 | (and (contains? renderer/filters filter) filter) 41 | (do (js/console.warn "INVALID FILTER: " (str filter)) :all)))}) 42 | 43 | (defn- update-some [m k f & more] 44 | (update m k (fn [v] (if-not v v (apply f v more))))) 45 | 46 | (defmethod m/mutate `sel/set-active-selectors [{:keys [state ast]} k {:keys [selectors]}] 47 | {:action #(swap! state update-some :selectors sel/set-selectors* selectors) 48 | :remote true}) 49 | 50 | (defrecord Router [] 51 | cp/Lifecycle 52 | (start [this] 53 | (let [{:keys [reconciler]} (-> this :test/renderer :app) 54 | history (new-history parse-fragment 55 | (fn on-route-change [{:keys [filter selectors]}] 56 | (when filter 57 | (om/transact! reconciler 58 | `[(set-page-filter 59 | ~{:filter filter})])) 60 | (om/transact! reconciler 61 | `[(sel/set-active-selectors 62 | ~{:selectors selectors})])))] 63 | (defmethod m/mutate `renderer/toggle-filter [{:keys [state]} _ {:keys [filter]}] 64 | {:action #(update-fragment! history :filter 65 | (let [new-filter (name filter)] 66 | (fn [old-filter] 67 | (if (= old-filter new-filter) 68 | "all" new-filter))))}) 69 | (defmethod m/mutate `sel/set-selector [{:keys [state]} _ new-selector] 70 | {:action #(assoc-fragment! history :selectors (sel/to-string (sel/set-selector* (:selectors @state) new-selector)))}) 71 | (defmethod m/mutate `sel/set-selectors [{:keys [state ast]} k {:keys [selectors]}] 72 | {:action #(assoc-fragment! history :selectors (or selectors (sel/to-string (:selectors @state))))}) 73 | (pushy/start! history) 74 | this)) 75 | (stop [this] 76 | (remove-method m/mutate `renderer/set-filter) 77 | (remove-method m/mutate `sel/set-selector) 78 | (remove-method m/mutate `sel/set-selectors) 79 | this)) 80 | 81 | (defn make-router [] 82 | (cp/using 83 | (map->Router {}) 84 | [:test/renderer])) 85 | -------------------------------------------------------------------------------- /src/untangled_spec/runner.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.runner 2 | (:require 3 | [clojure.spec :as s] 4 | [clojure.test :as t] 5 | [cljs.test #?@(:cljs (:include-macros true))] 6 | [com.stuartsierra.component :as cp] 7 | [untangled-spec.assertions :as ae] 8 | [untangled-spec.reporter :as reporter] 9 | [untangled-spec.selectors :as sel] 10 | [untangled-spec.spec :as us] 11 | #?@(:cljs ([om.next :as om] 12 | [untangled.client.mutations :as m] 13 | [untangled-spec.renderer :as renderer] 14 | [untangled-spec.router :as router])) 15 | #?@(:clj ([clojure.tools.namespace.repl :as tools-ns-repl] 16 | [clojure.walk :as walk] 17 | [cognitect.transit :as transit] 18 | [om.next.server :as oms] 19 | [ring.util.response :as resp] 20 | [untangled-spec.impl.macros :as im] 21 | [untangled-spec.watch :as watch] 22 | [untangled.server.core :as usc] 23 | [untangled.websockets.protocols :as ws] 24 | [untangled.websockets.components.channel-server :as wcs])))) 25 | 26 | #?(:clj 27 | (defmethod print-method Throwable [e w] 28 | (.write w (str e)))) 29 | 30 | #?(:clj 31 | (defn- ensure-encodable [tr] 32 | (letfn [(encodable? [x] 33 | (some #(% x) 34 | [number? string? symbol? keyword? sequential? 35 | (every-pred map? (comp not record?))]))] 36 | (walk/postwalk #(cond-> % (not (encodable? %)) pr-str) tr)))) 37 | 38 | #?(:clj 39 | (defn- send-renderer-msg 40 | ([system k edn cid] 41 | (let [cs (:channel-server system)] 42 | (ws/push cs cid k 43 | (ensure-encodable edn)))) 44 | ([system k edn] 45 | (->> system :channel-server 46 | :connected-cids deref :any 47 | (mapv (partial send-renderer-msg system k edn)))))) 48 | 49 | #?(:clj 50 | (defrecord ChannelListener [channel-server] 51 | ws/WSListener 52 | (client-dropped [this cs cid] this) 53 | (client-added [this cs cid] this) 54 | cp/Lifecycle 55 | (start [this] 56 | (wcs/add-listener wcs/listeners this) 57 | this) 58 | (stop [this] 59 | (wcs/remove-listener wcs/listeners this) 60 | this))) 61 | 62 | #?(:clj 63 | (defn- make-channel-listener [] 64 | (cp/using 65 | (map->ChannelListener {}) 66 | [:channel-server :test/reporter]))) 67 | 68 | (defn- novelty! [system mut-key novelty] 69 | #?(:cljs (om/transact! (om/app-root (get-in system [:test/renderer :test/renderer :app :reconciler])) 70 | `[(~mut-key ~novelty)]) 71 | :clj (send-renderer-msg system mut-key novelty))) 72 | 73 | (defn- render-tests [{:keys [test/reporter] :as runner}] 74 | (novelty! runner 'untangled-spec.renderer/render-tests 75 | (reporter/get-test-report reporter))) 76 | 77 | (defn run-tests [runner {:keys [refresh?] :or {refresh? false}}] 78 | (reporter/reset-test-report! (:test/reporter runner)) 79 | #?(:clj (when refresh? (tools-ns-repl/refresh))) 80 | (reporter/with-untangled-reporting 81 | runner render-tests 82 | ((:test! runner)))) 83 | 84 | (defrecord TestRunner [opts] 85 | cp/Lifecycle 86 | (start [this] 87 | #?(:cljs (let [runner-atom (-> this :test/renderer :test/renderer :runner-atom)] 88 | (reset! runner-atom this))) 89 | this) 90 | (stop [this] 91 | this)) 92 | 93 | (defn- make-test-runner [opts test! & [extra]] 94 | (cp/using 95 | (merge (map->TestRunner {:opts opts :test! test!}) 96 | extra) 97 | [:test/reporter #?(:clj :channel-server)])) 98 | 99 | (s/def ::test-paths (s/coll-of string?)) 100 | (s/def ::source-paths (s/coll-of string?)) 101 | (s/def ::ns-regex ::us/regex) 102 | (s/def ::port number?) 103 | (s/def ::config (s/keys :req-un [::port])) 104 | (s/def ::opts (s/keys :req-un [#?@(:cljs [::ns-regex]) 105 | #?@(:clj [::source-paths ::test-paths ::config])])) 106 | (s/fdef test-runner 107 | :args (s/cat 108 | :opts ::opts 109 | :test! fn? 110 | :renderer (s/? any?))) 111 | (defn test-runner [opts test! & [renderer]] 112 | #?(:cljs (cp/start 113 | (cp/system-map 114 | :test/runner (make-test-runner opts test! 115 | {:test/renderer renderer 116 | :read (fn [runner k params] 117 | {:value 118 | (case k 119 | :selectors (sel/get-current-selectors) 120 | (prn ::read k params))}) 121 | :mutate (fn [runner k params] 122 | {:action 123 | #(condp = k 124 | `sel/set-selector 125 | #_=> (do 126 | (sel/set-selector! params) 127 | (run-tests runner {})) 128 | `sel/set-active-selectors 129 | #_=> (do 130 | (sel/set-selectors! (:selectors params)) 131 | (run-tests runner {})) 132 | (prn ::mutate k params))})}) 133 | :test/reporter (reporter/make-test-reporter))) 134 | :clj (let [system (atom nil) 135 | api-read (fn [env k params] 136 | {:value 137 | (case k 138 | :selectors (sel/get-current-selectors) 139 | (prn ::read k params))}) 140 | api-mutate (fn [env k params] 141 | {:action 142 | #(condp = k 143 | `sel/set-selector 144 | #_=> (do 145 | (sel/set-selector! params) 146 | (run-tests (:test/runner @system) {})) 147 | `sel/set-active-selectors 148 | #_=> (do 149 | (sel/set-selectors! (:selectors params)) 150 | (run-tests (:test/runner @system) {})) 151 | (prn ::mutate k params))})] 152 | (reset! system 153 | (cp/start 154 | (usc/make-untangled-server 155 | :parser (oms/parser {:read api-read :mutate api-mutate}) 156 | :components {:config {:value (:config opts)} 157 | :channel-server (wcs/make-channel-server) 158 | :channel-listener (make-channel-listener) 159 | :test/runner (make-test-runner opts test!) 160 | :test/reporter (reporter/make-test-reporter) 161 | :change/watcher (watch/on-change-listener opts run-tests)} 162 | :extra-routes {:routes ["/" {"_untangled_spec_chsk" :web-socket 163 | "untangled-spec-server-tests.html" :server-tests}] 164 | :handlers {:web-socket wcs/route-handlers 165 | :server-tests (fn [{:keys [request]} _match] 166 | (resp/resource-response "untangled-spec-server-tests.html" 167 | {:root "public"}))}})))))) 168 | -------------------------------------------------------------------------------- /src/untangled_spec/selectors.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.selectors 2 | (:require 3 | #?(:cljs [cljs.reader :refer [read-string]]) 4 | [clojure.set :as set] 5 | [clojure.spec :as s] 6 | [untangled-spec.spec :as us] 7 | [untangled-spec.impl.selectors :refer [selectors]])) 8 | 9 | (s/def :selector/active? boolean?) 10 | (s/def :selector/id keyword?) 11 | (s/def ::selector (s/keys :req [:selector/id :selector/active?])) 12 | (s/def ::selectors (s/coll-of ::selector :kind vector? :into [])) 13 | (s/def ::shorthand (s/coll-of keyword? :kind set? :into #{})) 14 | (s/def ::default ::shorthand) 15 | (s/def ::available ::shorthand) 16 | (s/def ::initial-selectors (s/keys :req-un [::available] :opt-un [::default])) 17 | (s/def ::test-selectors (s/and (s/conformer #(if (seq %) (set %) #{::none})) ::shorthand)) 18 | 19 | (s/fdef parse-selectors 20 | :args (s/cat :selectors-str string?) 21 | :ret ::shorthand) 22 | (defn parse-selectors [selectors-str] 23 | (read-string selectors-str)) 24 | 25 | (s/fdef to-string 26 | :args (s/cat :selectors ::selectors) 27 | :ret string?) 28 | (defn to-string [selectors] 29 | (str 30 | (into #{} 31 | (comp (filter :selector/active?) (map :selector/id)) 32 | selectors))) 33 | 34 | (s/fdef get-current-selectors 35 | :ret ::selectors) 36 | (defn get-current-selectors [] 37 | (:current @selectors)) 38 | 39 | (s/fdef get-default-selectors 40 | :ret ::selectors) 41 | (defn get-default-selectors [] 42 | (:default @selectors)) 43 | 44 | (s/fdef initialize-selectors! 45 | :args (s/cat :initial-selectors ::initial-selectors)) 46 | (defn initialize-selectors! [{:keys [available default] 47 | :or {default #{::none}}}] 48 | (swap! selectors assoc :current 49 | (mapv (fn [sel] {:selector/id sel :selector/active? (contains? default sel)}) 50 | (conj available ::none))) 51 | (swap! selectors assoc :default default) 52 | true) 53 | 54 | (s/fdef set-selectors* 55 | :args (s/cat 56 | :current-selectors ::selectors 57 | :new-selectors ::shorthand) 58 | :ret ::selectors) 59 | (defn set-selectors* [current-selectors new-selectors] 60 | (mapv (fn [{:as sel :keys [selector/id]}] 61 | (assoc sel :selector/active? (contains? new-selectors id))) 62 | current-selectors)) 63 | 64 | (defn set-selectors! [test-selectors] 65 | (swap! selectors update :current set-selectors* 66 | (or test-selectors (:default @selectors)))) 67 | 68 | (s/fdef set-selector* 69 | :args (s/cat 70 | :current-selectors ::selectors 71 | :new-selector ::selector) 72 | :ret ::selectors) 73 | (defn set-selector* [current-selectors {:keys [selector/id selector/active?]}] 74 | (mapv (fn [sel] 75 | (cond-> sel (= (:selector/id sel) id) 76 | (assoc :selector/active? active?))) 77 | current-selectors)) 78 | 79 | (defn set-selector! [selector] 80 | (swap! selectors update :current set-selectors* selector)) 81 | 82 | (s/fdef selected-for?* 83 | :args (s/cat 84 | :current-selectors ::selectors 85 | :test-selectors ::test-selectors) 86 | :ret boolean?) 87 | (defn selected-for?* [current-selectors test-selectors] 88 | (boolean 89 | (or 90 | ;;not defined test selectors always run 91 | (seq (set/difference test-selectors (into #{} (map :selector/id) current-selectors))) 92 | ;;1+ test selector are active 93 | (seq (set/intersection test-selectors 94 | (into #{} (comp (filter :selector/active?) (map :selector/id)) 95 | current-selectors)))))) 96 | 97 | (defn selected-for? [test-selectors] 98 | (selected-for?* (:current @selectors) test-selectors)) 99 | -------------------------------------------------------------------------------- /src/untangled_spec/spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.spec 2 | (:require 3 | [clojure.spec :as s])) 4 | 5 | (defn conform! [spec x] 6 | (let [rt (s/conform spec x)] 7 | (when (s/invalid? rt) 8 | (throw (ex-info (s/explain-str spec x) 9 | (s/explain-data spec x)))) 10 | rt)) 11 | 12 | (s/def ::any any?) 13 | 14 | (defn regex? [x] (= (type x) (type #""))) 15 | (s/def ::regex regex?) 16 | -------------------------------------------------------------------------------- /src/untangled_spec/spec_renderer.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.spec-renderer 2 | (:require 3 | [untangled-spec.suite :as suite])) 4 | 5 | (enable-console-print!) 6 | 7 | (defonce renderer (suite/test-renderer)) 8 | -------------------------------------------------------------------------------- /src/untangled_spec/stub.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.stub 2 | #?(:clj 3 | (:require [clojure.test :refer (is)])) 4 | #?(:cljs (:require-macros [cljs.test :refer (is)]))) 5 | 6 | (defn make-step [stub times literals] 7 | {:stub stub :times times 8 | :ncalled 0 :literals literals 9 | :history []}) 10 | (defn make-script [function steps] 11 | (atom {:function function :steps steps :history []})) 12 | 13 | (defn increment-script-call-count [script-atom step] 14 | (swap! script-atom update-in [:steps step :ncalled] inc)) 15 | 16 | (defn step-complete [script-atom step] 17 | (let [{:keys [ncalled times]} 18 | (get-in @script-atom [:steps step])] 19 | (= ncalled times))) 20 | 21 | (defn zip-pad [pad & colls] 22 | (let [ncolls (count colls)] 23 | (->> colls 24 | (map #(concat % ((if (fn? pad) repeatedly repeat) pad))) 25 | (apply interleave) 26 | (take (* ncolls (apply max (map count colls)))) 27 | (partition ncolls)))) 28 | 29 | (defn valid-args? [literals args] 30 | (or (not literals) 31 | (let [reduced-if (fn [p x] (cond-> x p reduced))] 32 | (reduce (fn [_ [lit arg]] 33 | (reduced-if false? 34 | (case lit 35 | ::&_ (reduced true) 36 | ::any true 37 | (= lit arg)))) 38 | true (zip-pad gensym literals args))))) 39 | 40 | (defn scripted-stub [script-atom] 41 | (let [step (atom 0)] 42 | (fn [& args] 43 | (let [{:keys [function steps ncalled]} @script-atom 44 | max-calls (count steps) 45 | curr-step @step] 46 | (if (>= curr-step max-calls) 47 | (throw (ex-info (str function " was called too many times!") 48 | {:max-calls max-calls 49 | :args args})) 50 | (let [{:keys [stub literals]} (nth steps curr-step)] 51 | (when-not (valid-args? literals args) 52 | (throw (ex-info (str function " was called with wrong arguments") 53 | {:args args :expected-literals literals}))) 54 | (swap! script-atom 55 | #(-> % (update :history conj args) 56 | (update-in [:steps curr-step :history] conj args))) 57 | (try (apply stub args) 58 | (catch #?(:clj Exception :cljs js/Object) e (throw e)) 59 | (finally 60 | (increment-script-call-count script-atom curr-step) 61 | (when (step-complete script-atom curr-step) 62 | (swap! step inc)))))))))) 63 | 64 | (defn validate-step-counts 65 | "argument step contains keys: 66 | - :ncalled, actual number of times called 67 | - :times, expected number of times called" 68 | [errors {:as step :keys [ncalled times]}] 69 | (conj errors 70 | (if (or (= ncalled times) 71 | (and (= times :many) 72 | (> ncalled 0))) 73 | :ok :error))) 74 | 75 | (defn validate-target-function-counts [script-atoms] 76 | (mapv (fn [script] 77 | (let [{:keys [function steps history]} @script 78 | count-results (reduce validate-step-counts [] steps) 79 | errors? (some #(= :error %) count-results)] 80 | (when errors? 81 | (throw (ex-info (str function " was not called as many times as specified") 82 | @script))))) 83 | script-atoms)) 84 | -------------------------------------------------------------------------------- /src/untangled_spec/suite.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.suite 2 | #?(:cljs (:require-macros [untangled-spec.suite])) 3 | (:require 4 | [clojure.spec :as s] 5 | [com.stuartsierra.component :as cp] 6 | [untangled-spec.runner :as runner] 7 | [untangled-spec.selectors :as sel] 8 | [untangled-spec.spec :as us] 9 | #?@(:cljs ([untangled-spec.renderer :as renderer] 10 | [untangled-spec.router :as router])) 11 | #?@(:clj ([clojure.java.io :as io] 12 | [clojure.tools.namespace.find :as tools-ns-find] 13 | [clojure.tools.namespace.repl :as tools-ns-repl] 14 | [untangled-spec.impl.macros :as im] 15 | [untangled-spec.impl.runner :as ir])))) 16 | 17 | #?(:cljs 18 | (defn test-renderer 19 | "FOR INTERNAL (DEV TIME) UNTANGLED_SPEC USE ONLY 20 | 21 | WARNING: You should not need to use this directly, instead you should be starting a server `def-test-suite` 22 | and going to `localhost:PORT/untangled-spec-server-tests.html`. 23 | 24 | Creates a renderer for server (clojure) tests when using `def-test-suite`." 25 | [& [opts]] 26 | (cp/start 27 | (cp/system-map 28 | :test/renderer (renderer/make-test-renderer opts) 29 | :test/router (router/make-router))))) 30 | 31 | #?(:clj 32 | (defn- make-test-fn [env opts] 33 | `(fn [] 34 | ~(if (im/cljs-env? env) 35 | `(cljs.test/run-all-tests ~(:ns-regex opts) 36 | (cljs.test/empty-env ::TestRunner)) 37 | `(let [test-nss# 38 | (mapcat (comp tools-ns-find/find-namespaces-in-dir io/file) 39 | ~(:test-paths opts))] 40 | (apply require test-nss#) 41 | (apply clojure.test/run-tests test-nss#)))))) 42 | 43 | #?(:clj 44 | (defmacro def-test-suite 45 | "For use in defining a untangled-spec test suite. 46 | ARGUMENTS: 47 | * `suite-name` is the name (symbol) of the test suite function you can call, see each section below for details. 48 | * `opts` map containing configuration for the test suite, see each section below for details. 49 | * `selectors` contains `:available` and `:default` sets of keyword selectors for your tests. See `untangled-spec.selectors` for more info. 50 | 51 | CLOJURESCRIPT: 52 | * Make sure you are defining a cljsbuild that emits your client tests to `js/test/test.js`. 53 | * You can call the function `suite-name` repeatedly, it will just re-run the tests. 54 | * `opts` should contain : 55 | ** `:ns-regex` that will be used to filter to just your test namespaces. 56 | 57 | CLOJURE: 58 | * Defines a function `suite-name` that restarts the webserver, presumably with new configuration. 59 | * Starts a webserver that serves `localhost:PORT/untangled-spec-server-tests.html`, (see `opts` description) 60 | * `opts` should contain : 61 | ** `:test-paths` : used to find your test files. 62 | ** `:source-paths` : concatenated with `test-paths` to create watch and refresh dirs, so that we can scan those directories for changes and refresh those namespaces whenever they change. 63 | ** `:config` : should contain any necessary configuration for the webserver, should just be `:port` (eg: {:config {:port 8888}}) 64 | " 65 | [suite-name opts selectors] 66 | (if (im/cljs-env? &env) 67 | ;;BROWSER 68 | `(let [test!# ~(make-test-fn &env opts)] 69 | (defonce _# (sel/initialize-selectors! ~selectors)) 70 | (defonce renderer# (test-renderer {:with-websockets? false})) 71 | (def test-system# (runner/test-runner ~opts test!# renderer#)) 72 | (defn ~suite-name [] 73 | (runner/run-tests (:test/runner test-system#) {}))) 74 | ;;SERVER 75 | `(let [test!# ~(make-test-fn &env opts)] 76 | (sel/initialize-selectors! ~selectors) 77 | (defn stop# [] (swap! ir/runner (comp (constantly {}) cp/stop))) 78 | (defn start# [] (reset! ir/runner (runner/test-runner ~opts test!#))) 79 | (defn ~suite-name [] (stop#) (start#)))))) 80 | 81 | #?(:clj 82 | (defmacro test-suite-internal 83 | "FOR INTERNAL (DEV TIME) UNTANGLED_SPEC USE ONLY 84 | 85 | WARNING: You should not need to use this directly, 86 | instead you should be starting a server `def-test-suite` 87 | and going to `localhost:PORT/untangled-spec-server-tests.html`. 88 | 89 | Creates a webserver for clojure tests, can be `start` and `stop` -ed. 90 | This is in case you need to refresh your namespaces in your server restarts, 91 | however this should only be necessary when developing untangled-spec itself, 92 | as once it's in a jar it can't be refreshed." 93 | [opts selectors] 94 | `(let [test!# ~(make-test-fn &env opts)] 95 | (sel/initialize-selectors! ~selectors) 96 | (runner/test-runner ~opts test!#)))) 97 | -------------------------------------------------------------------------------- /src/untangled_spec/watch.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.watch 2 | (:require 3 | [clojure.tools.namespace.dir :as tools-ns-dir] 4 | [clojure.tools.namespace.find :refer [clj]] 5 | [clojure.tools.namespace.track :as tools-ns-track] 6 | [clojure.tools.namespace.repl :as tools-ns-repl] 7 | [com.stuartsierra.component :as cp])) 8 | 9 | (defn- make-change-tracker [watch-dirs] 10 | (tools-ns-dir/scan-dirs (tools-ns-track/tracker) watch-dirs {:platform clj})) 11 | 12 | (defn- scan-for-changes [tracker watch-dirs] 13 | (try (let [new-tracker (tools-ns-dir/scan-dirs tracker watch-dirs {:platform clj})] 14 | new-tracker) 15 | (catch Exception e 16 | (.printStackTrace e) 17 | ;; return the same tracker so we dont try to run tests 18 | tracker))) 19 | 20 | (defmacro async [& body] 21 | `(let [ns# *ns*] 22 | (.start 23 | (Thread. 24 | (fn [] 25 | (binding [*ns* ns#] 26 | ~@body)))))) 27 | 28 | (defn something-changed? [new-tracker curr-tracker] 29 | (not= new-tracker curr-tracker)) 30 | 31 | (defrecord ChangeListener [watching? watch-dirs run-tests] 32 | cp/Lifecycle 33 | (start [this] 34 | (apply tools-ns-repl/set-refresh-dirs watch-dirs) 35 | (async 36 | (loop [tracker (make-change-tracker watch-dirs)] 37 | (let [new-tracker (scan-for-changes tracker watch-dirs)] 38 | (when @watching? 39 | (when (something-changed? new-tracker tracker) 40 | (try (run-tests (:test/runner this) {:refresh? true}) 41 | (catch Exception e (.printStackTrace e)))) 42 | (do (Thread/sleep 200) 43 | (recur (dissoc new-tracker 44 | ::tools-ns-track/load 45 | ::tools-ns-track/unload))))))) 46 | this) 47 | (stop [this] 48 | (reset! watching? false) 49 | this)) 50 | 51 | (defn on-change-listener [{:keys [source-paths test-paths]} run-tests] 52 | (cp/using 53 | (map->ChangeListener 54 | {:watching? (atom true) 55 | :watch-dirs (concat source-paths test-paths) 56 | :run-tests run-tests}) 57 | [:test/runner])) 58 | -------------------------------------------------------------------------------- /test/untangled_spec/all_tests.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.all-tests 2 | (:require 3 | untangled-spec.tests-to-run ;; ensures tests are loaded so doo can find them 4 | [doo.runner :refer-macros [doo-all-tests]])) 5 | 6 | ;;entry point for CI cljs tests, see github.com/bensu/doo 7 | (doo-all-tests #"untangled-spec\..*-spec") 8 | -------------------------------------------------------------------------------- /test/untangled_spec/assertions_spec.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.assertions-spec 2 | (:require 3 | [clojure.spec :as s] 4 | [clojure.test :as t :refer [is]] 5 | [untangled-spec.assertions :as ae 6 | :refer [check-error check-error* parse-criteria]] 7 | [untangled-spec.contains :refer [*contains?]] 8 | [untangled-spec.core 9 | :refer [specification component behavior assertions]] 10 | [untangled-spec.impl.macros :as im] 11 | [untangled-spec.spec :as us] 12 | [untangled-spec.testing-helpers :as th]) 13 | (:import clojure.lang.ExceptionInfo)) 14 | 15 | (defn check-assertion [expected] 16 | (fn [actual] 17 | (and 18 | (->> actual first (= 'clojure.test/is)) 19 | (->> actual second (= expected))))) 20 | 21 | (defn test-triple->assertion [form] 22 | (ae/triple->assertion false (us/conform! ::ae/triple form))) 23 | 24 | (defn test-block->asserts [form] 25 | (ae/block->asserts false (us/conform! ::ae/block form))) 26 | 27 | (def test-regex #"a-simple-test-regex") 28 | 29 | (specification "check-error" 30 | (behavior "supports many syntaxes" 31 | (assertions 32 | (us/conform! ::ae/criteria 'ExceptionInfo) 33 | => [:sym 'ExceptionInfo] 34 | (us/conform! ::ae/criteria {:ex-type 'ExceptionInfo 35 | :fn even?, :regex test-regex}) 36 | => [:map {:ex-type 'ExceptionInfo :fn even? :regex test-regex}] 37 | (us/conform! ::ae/criteria ['ExceptionInfo]) 38 | => [:list {:ex-type 'ExceptionInfo}] 39 | 40 | (parse-criteria [:sym 'irr]) => {:ex-type 'irr} 41 | (parse-criteria [:w/e 'dont-care]) => 'dont-care 42 | 43 | (check-error "spec-msg1" (ex-info "foo" {}) 44 | {:ex-type ExceptionInfo}) 45 | =fn=> (*contains? {:type :pass}) 46 | (let [check #(-> % ex-data :ok)] 47 | (check-error "spec-msg2" (ex-info "foo" {:ok false}) 48 | {:fn check} '#(some fn))) 49 | =fn=> (*contains? {:type :fail :expected '#(some fn)}) 50 | (check-error "spec-msg3" (ex-info "foo" {}) 51 | {:regex #"oo"}) 52 | =fn=> (*contains? {:type :pass}))) 53 | (behavior "checks the exception is of the specified type or throws" 54 | (assertions 55 | (check-error* "msg1" (ex-info "foo" {}) 56 | clojure.lang.ExceptionInfo) 57 | =fn=> (*contains? {:type :pass :message "msg1"}) 58 | (check-error* "msg2" (ex-info "foo" {}) 59 | java.lang.Error) 60 | =fn=> (*contains? {:type :fail :extra "exception did not match type" 61 | :actual clojure.lang.ExceptionInfo :expected java.lang.Error}))) 62 | (behavior "checks the exception's message matches a regex or throws" 63 | (assertions 64 | (check-error* "msg3" (ex-info "Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn" {}) 65 | clojure.lang.ExceptionInfo #"(?i)cthulhu") 66 | =fn=> (*contains? {:type :pass}) 67 | (check-error* "msg4" (ex-info "kthxbye" {}) 68 | clojure.lang.ExceptionInfo #"cthulhu") 69 | =fn=> (*contains? {:type :fail :extra "exception's message did not match regex" 70 | :actual "kthxbye" :expected "cthulhu"}))) 71 | (behavior "checks the exception with the user's function" 72 | (let [cthulhu-bored (ex-info "Haskell 101" {:cthulhu :snores})] 73 | (assertions 74 | (check-error* "msg5" (ex-info "H.P. Lovecraft" {:cthulhu :rises}) 75 | clojure.lang.ExceptionInfo #"(?i)lovecraft" 76 | #(-> % ex-data :cthulhu (= :rises))) 77 | =fn=> (*contains? {:type :pass}) 78 | (check-error* "msg6" cthulhu-bored 79 | clojure.lang.ExceptionInfo #"Haskell" 80 | #(-> % ex-data :cthulhu (= :rises))) 81 | =fn=> (*contains? {:type :fail :actual cthulhu-bored 82 | :extra "checker function failed"}))))) 83 | 84 | (specification "triple->assertion" 85 | (behavior "checks equality with the => arrow" 86 | (assertions 87 | (test-triple->assertion '(left => right)) 88 | =fn=> (check-assertion '(= right left)))) 89 | (behavior "verifies actual with the =fn=> function" 90 | (assertions 91 | (test-triple->assertion '(left =fn=> right)) 92 | =fn=> (check-assertion '(exec right left)))) 93 | (behavior "verifies that actual threw an exception with the =throws=> arrow" 94 | (assertions 95 | (test-triple->assertion '(left =throws=> right)) 96 | =fn=> (check-assertion '(throws? false left right)))) 97 | (behavior "any other arrow, throws an ex-info" 98 | (assertions 99 | (test-triple->assertion '(left =bad-arrow=> right)) 100 | =throws=> (ExceptionInfo #"fails spec.*arrow")))) 101 | 102 | (specification "throws assertion arrow" 103 | (behavior "catches AssertionErrors" 104 | (let [f (fn [x] {:pre [(even? x)]} (inc x))] 105 | (is (thrown? AssertionError (f 1))) 106 | (is (= 3 (f 2))) 107 | (assertions 108 | (f 1) =throws=> (AssertionError #"even\? x") 109 | (f 6) => 7 110 | (f 2) => 3)))) 111 | 112 | (defn get-exp-act [{exp :expected act :actual msg :message extra :extra} & [opt]] 113 | (case opt 114 | :all [act exp msg extra] 115 | :msg [act exp msg] 116 | :ae [act exp extra] 117 | [act exp])) 118 | 119 | (defmacro test-case [x & [opt]] 120 | `(binding [t/report identity] 121 | (get-exp-act ~(-> x test-triple->assertion) ~opt))) 122 | 123 | (specification "running assertions reports the correct data" 124 | (component "=>" 125 | (behavior "literals" 126 | (is (= [5 3] (test-case (5 => 3))))) 127 | (behavior "forms" 128 | (is (= [5 3 "(+ 3 2) => (+ 2 1)"] 129 | (test-case ((+ 3 2) => (+ 2 1)) :msg)))) 130 | (behavior "unexpected throw" 131 | (is (= ["clojure.lang.ExceptionInfo: bad {}" 132 | "(= \"good\" (throw (ex-info \"bad\" {})))" 133 | "(throw (ex-info \"bad\" {})) => good"] 134 | (mapv str (test-case ((throw (ex-info "bad" {})) => "good") :msg)))))) 135 | 136 | (component "=fn=>" 137 | (behavior "literals" 138 | (is (= [5 'even? "5 =fn=> even?"] 139 | (test-case (5 =fn=> even?) :msg)))) 140 | (behavior "lambda" 141 | (is (re-find #"even\?" 142 | (str (second (test-case (7 =fn=> #(even? %)))))))) 143 | (behavior "forms" 144 | (is (= [7 '(fn [x] (even? x))] 145 | (test-case ((+ 5 2) =fn=> (fn [x] (even? x))))))) 146 | (behavior "unexpected error" 147 | (is (= ["clojure.lang.ExceptionInfo: bad {}" 148 | "(exec even? (throw (ex-info \"bad\" {})))" 149 | "(throw (ex-info \"bad\" {})) =fn=> even?"] 150 | (mapv str (test-case ((throw (ex-info "bad" {})) =fn=> even?) :msg)))))) 151 | 152 | (component "=throws=>" 153 | (behavior "reports if the message didnt match the regex" 154 | (is (= ["foo", "asdf", "(throw (ex-info \"foo\" {})) =throws=> (clojure.lang.ExceptionInfo #\"asdf\")" 155 | "exception's message did not match regex"] 156 | (test-case ((throw (ex-info "foo" {})) 157 | =throws=> (clojure.lang.ExceptionInfo #"asdf")) 158 | :all)))) 159 | (behavior "reports if nothing was thrown" 160 | (is (= ["it to throw", "(+ 5 2) =throws=> (clojure.lang.ExceptionInfo #\"asdf\")" 161 | "Expected an error to be thrown!"] 162 | (-> ((+ 5 2) =throws=> (clojure.lang.ExceptionInfo #"asdf")) 163 | (test-case :all) rest))))) 164 | 165 | (component "block->asserts" 166 | (behavior "wraps triples in behavior do-reports" 167 | (let [reporting (th/locate `im/with-reporting (test-block->asserts '("string2" d => e)))] 168 | (is (= `(im/with-reporting {:type :behavior :string "string2"}) 169 | (take 2 reporting))))) 170 | (behavior "converts triples to assertions" 171 | (let [asserts (test-block->asserts '("string2" d => e))] 172 | (is (every? #{`t/is} (map first (drop 2 asserts)))))))) 173 | 174 | (specification "fix-conform for issue #31" 175 | (assertions 176 | (mapv (juxt :behavior (comp count :triples)) 177 | (ae/fix-conform 178 | (us/conform! ::ae/assertions 179 | '("foo" 1 => 2 "bar" 3 => 4, 5 => 6 "qux" 7 => 8, 9 => 10)))) 180 | => '[["foo" 1] ["bar" 2] ["qux" 2]])) 181 | -------------------------------------------------------------------------------- /test/untangled_spec/assertions_spec.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.assertions-spec 2 | (:require [untangled-spec.core :refer-macros [specification assertions]])) 3 | 4 | (specification "assertions blocks work on cljs" 5 | (assertions 6 | "throws arrow can catch" 7 | (assert false "foobar") =throws=> (js/Error #"ooba") 8 | "throws arrow can catch js/Objects" 9 | (throw #js {}) =throws=> (js/Object))) 10 | -------------------------------------------------------------------------------- /test/untangled_spec/async_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.async-spec 2 | (:require [untangled-spec.async :as async] 3 | [untangled-spec.core #?(:clj :refer :cljs :refer-macros) 4 | [specification behavior provided 5 | with-timeline async tick assertions]] 6 | #?(:clj [clojure.test :refer [is]]) 7 | #?(:cljs [cljs.test :refer-macros [is]]))) 8 | 9 | (defn mock-fn1 [] (identity 0)) 10 | (defn mock-fn2 [] (identity 0)) 11 | (defn mock-fn3 [] (identity 0)) 12 | 13 | (specification "async-queue" 14 | (behavior "associates an event correctly with its time" 15 | (let [queue (async/make-async-queue)] 16 | 17 | (async/schedule-event queue 1044 mock-fn3) 18 | (async/schedule-event queue 144 mock-fn2) 19 | (async/schedule-event queue 44 mock-fn1) 20 | 21 | (is (= mock-fn1 (:fn-to-call (get @(:schedule queue) 44)))) 22 | (is (= mock-fn2 (:fn-to-call (get @(:schedule queue) 144)))) 23 | (is (= mock-fn3 (:fn-to-call (get @(:schedule queue) 1044)))) 24 | (is (= 44 (:abs-time (get @(:schedule queue) 44)))) 25 | (is (= 144 (:abs-time (get @(:schedule queue) 144)))) 26 | (is (= 1044 (:abs-time (get @(:schedule queue) 1044)))) 27 | )) 28 | 29 | 30 | (behavior "keeps events in order" 31 | (let [queue (async/make-async-queue)] 32 | 33 | (async/schedule-event queue 1044 mock-fn3) 34 | (async/schedule-event queue 144 mock-fn2) 35 | (async/schedule-event queue 44 mock-fn1) 36 | 37 | (is (= [44 144 1044] (keys @(:schedule queue)))) 38 | ) 39 | ) 40 | 41 | 42 | 43 | (behavior "refuses to add events that collide in time" 44 | (let [queue (async/make-async-queue)] 45 | 46 | (async/schedule-event queue 44 identity) 47 | 48 | #?(:clj (is (thrown-with-msg? java.lang.Exception #"already contains an event" (async/schedule-event queue 44 identity))) 49 | :cljs (is (thrown-with-msg? js/Error #"already contains an event" (async/schedule-event queue 44 identity))) 50 | ) 51 | ) 52 | ) 53 | 54 | (behavior "associates an event correctly with its time relative to current-time" 55 | (let [queue (async/make-async-queue)] 56 | (async/schedule-event queue 44 mock-fn1) 57 | 58 | (async/advance-clock queue 10) 59 | (async/schedule-event queue 44 mock-fn2) 60 | 61 | (is (= mock-fn1 (:fn-to-call (get @(:schedule queue) 44)))) 62 | (is (= mock-fn2 (:fn-to-call (get @(:schedule queue) 54)))) 63 | (is (= 44 (:abs-time (get @(:schedule queue) 44)))) 64 | (is (= 54 (:abs-time (get @(:schedule queue) 54)))) 65 | ) 66 | ) 67 | 68 | (behavior "executes and removes events as clock advances" 69 | (let [detector (atom false) 70 | detect (fn [] (reset! detector true)) 71 | queue (async/make-async-queue)] 72 | (async/schedule-event queue 44 detect) 73 | (async/schedule-event queue 144 mock-fn1) 74 | 75 | (async/advance-clock queue 50) 76 | 77 | (is @detector) 78 | (is (not (nil? (async/peek-event queue)))) 79 | ) 80 | ) 81 | 82 | (behavior "advance clock just advances the time with no events" 83 | (let [queue (async/make-async-queue)] 84 | 85 | (async/advance-clock queue 1050) 86 | 87 | (is (= 1050 (async/current-time queue))) 88 | ) 89 | ) 90 | 91 | (behavior "passes exceptions through to caller of advance-clock" 92 | (let [queue (async/make-async-queue) 93 | thrower 94 | #?(:cljs (fn [] (throw (js/Error. "Bummer!"))) 95 | :clj (fn [] (throw (java.lang.Exception. "Bummer!")))) 96 | ] 97 | (async/schedule-event queue 10 thrower) 98 | #?(:clj (is (thrown? java.lang.Exception (async/advance-clock queue 100))) 99 | :cljs (is (thrown? js/Error (async/advance-clock queue 100)))) 100 | ) 101 | ) 102 | 103 | (behavior "triggers events in correct order when a triggered event adds to queue" 104 | (let [queue (async/make-async-queue) 105 | invocations (atom 0) ;how many functions have run 106 | add-on-fn (fn [] ; scheduled by initial function (just below this one) 10ms AFTER it runs (abs of 11ms) 107 | (is (= 11 (async/current-time queue))) 108 | (is (= 1 @invocations)) 109 | (swap! invocations inc)) 110 | trigger-adding-evt (fn [] ; scheduled below to run at 1ms 111 | (is (= 0 @invocations)) 112 | (is (= 1 (async/current-time queue))) 113 | (swap! invocations inc) 114 | (async/schedule-event queue 10 add-on-fn) 115 | ) 116 | late-fn (fn [] ; manually scheduled at 15ms...must run AFTER the one that was added during the trigger 117 | (is (= 15 (async/current-time queue))) 118 | (is (= 2 @invocations)) 119 | ) 120 | ] 121 | 122 | (async/schedule-event queue 1 trigger-adding-evt) 123 | (async/schedule-event queue 15 late-fn) 124 | (async/advance-clock queue 100) 125 | ;; see assertions in functions... 126 | ) 127 | ) 128 | ) 129 | 130 | 131 | (specification "processing-an-event" 132 | (behavior "executes the first scheduled item" 133 | (let [detector (atom false) 134 | detect (fn [] (reset! detector true)) 135 | queue (async/make-async-queue)] 136 | (async/schedule-event queue 44 detect) 137 | 138 | (async/process-first-event! queue) 139 | 140 | (is @detector) 141 | )) 142 | 143 | 144 | (behavior "removes the first scheduled item" 145 | (let [queue (async/make-async-queue)] 146 | (async/schedule-event queue 44 mock-fn1) 147 | 148 | (async/process-first-event! queue) 149 | 150 | (is (= 0 (count @(:schedule queue)))) 151 | 152 | ) 153 | )) 154 | 155 | -------------------------------------------------------------------------------- /test/untangled_spec/contains_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.contains-spec 2 | (:require [untangled-spec.core #?(:clj :refer :cljs :refer-macros) 3 | [specification behavior assertions]] 4 | [untangled-spec.contains :refer [*contains?]])) 5 | 6 | (specification "untangled-spec.contains *contains?" 7 | (behavior "can check that a string" 8 | (behavior "contains a string" 9 | (assertions 10 | (str "somestring") =fn=> (*contains? "mestri"))) 11 | (behavior "contains a regex" 12 | (assertions 13 | (str "abrpij") =fn=> (*contains? #"pij$")))) 14 | (behavior "can check that a map" 15 | (behavior "contains a subset" 16 | (assertions 17 | {:k1 :v1 :k2 :v2} =fn=> (*contains? {:k1 :v1}))) 18 | (behavior "contains certain keys" 19 | (assertions 20 | {:k1 :v1 :k2 :v2} =fn=> (*contains? [:k1 :k2] :keys))) 21 | (behavior "contains certain values" 22 | (assertions 23 | {:k1 :v1 :k2 :v2} =fn=> (*contains? [:v1 :v2] :vals)))) 24 | (behavior "can check that a set" 25 | (behavior "contains 1+ in another seq" 26 | (assertions 27 | #{8 6 3 2} =fn=> (*contains? #{0 1 2}))) 28 | (behavior "contains all in another seq" 29 | (assertions 30 | #{4 9 5 0} =fn=> (*contains? [0 4 5])))) 31 | (behavior "can check that a list/vector" 32 | (behavior "contains 1+ in another seq" 33 | (assertions 34 | [2 4 7] =fn=> (*contains? #{0 1 2}))) 35 | (behavior "contains a subseq" 36 | (assertions 37 | [1 2] =fn=> (*contains? [1 2]) 38 | [1 2] =fn=> (comp not (*contains? [2 1])) 39 | [1 21] =fn=> (comp not (*contains? [1 2])) 40 | [12 1] =fn=> (comp not (*contains? [1 2])) 41 | )) 42 | (behavior "contains a subseq with gaps" 43 | #_(assertions 44 | [3 7 0 1] =fn=> (*contains? [3 1] :gaps) 45 | [3 7 0 1] =fn=> (*contains? [3 7] :gaps) 46 | [3 7 0 1] =fn=> (comp not (*contains? [1 0] :gaps)) 47 | )) 48 | (behavior "contains a subseq in any order" 49 | #_(assertions 50 | [34 7 1 87] =fn=> (*contains? [87 1] :any-order) 51 | )) 52 | (behavior "contains a subseq with gaps & in any order" 53 | #_(assertions 54 | [98 32 78 16] =fn=> (*contains? [16 98] :both) 55 | )))) 56 | -------------------------------------------------------------------------------- /test/untangled_spec/core_spec.clj: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.core-spec 2 | (:require 3 | [clojure.test :as t :refer [is]] 4 | [untangled-spec.contains :refer [*contains?]] 5 | [untangled-spec.core 6 | :refer [specification behavior when-mocking assertions] 7 | :as core] 8 | [untangled-spec.selectors :as sel])) 9 | 10 | (specification "adds methods to clojure.test/assert-expr" 11 | (assertions 12 | (methods t/assert-expr) 13 | =fn=> (*contains? '[= exec throws?] :keys))) 14 | (specification "var-name-from-string" 15 | (assertions 16 | "allows the following" 17 | (core/var-name-from-string "asdfASDF1234!#$%&*|:<>?") 18 | =fn=> #(not (re-find #"\-" (str %))) 19 | "converts the rest to dashes" 20 | (core/var-name-from-string "\\\"@^()[]{};',/ ∂¨∫øƒ∑Ó‡fi€⁄ª•¶§¡˙√ß") 21 | =fn=> #(re-matches #"__\-+__" (str %)))) 22 | 23 | (defmacro test-core [code-block test-fn] 24 | `(let [test-var# ~code-block 25 | reports# (atom [])] 26 | (binding [t/report #(swap! reports# conj %)] 27 | (with-redefs [sel/selected-for? (constantly true)] 28 | (test-var#))) 29 | (alter-meta! test-var# dissoc :test) 30 | (~test-fn @reports#))) 31 | 32 | (specification "uncaught errors are gracefully handled & reported" 33 | (let [only-errors (comp 34 | (filter (comp #{:error} :type)) 35 | (map #(select-keys % [:type :actual :message :expected])) 36 | (map #(update % :actual str)))] 37 | (assertions 38 | (test-core (specification "ERROR INTENTIONAL" :should-fail 39 | (assert false)) 40 | (partial into [] only-errors)) 41 | => [{:type :error 42 | :actual "java.lang.AssertionError: Assert failed: false" 43 | :message "ERROR INTENTIONAL" 44 | :expected "IT TO NOT THROW!"}] 45 | (test-core (specification "EXPECTED ERROR IN BEHAVIOR" :should-fail 46 | (behavior "SHOULD ERROR" 47 | (assert false))) 48 | (partial into [] only-errors)) 49 | => [{:type :error 50 | :actual "java.lang.AssertionError: Assert failed: false" 51 | :message "SHOULD ERROR" 52 | :expected "IT TO NOT THROW!"}]))) 53 | -------------------------------------------------------------------------------- /test/untangled_spec/diff_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.diff-spec 2 | (:require 3 | [untangled-spec.diff :as src 4 | :refer [nf diff diff-elem patch compress decompress]] 5 | [untangled-spec.core :as usc :refer 6 | [specification behavior assertions]])) 7 | 8 | (specification "the diff function" 9 | (assertions 10 | "returns {} for non collections" 11 | (diff 0 1) => {}) 12 | 13 | (assertions "returns 'simple' diff if different types" 14 | (diff [{}] [#{}]) 15 | => {[0] (diff-elem {} #{})} 16 | (diff {0 {}} {0 #{}}) 17 | => {[0] (diff-elem {} #{})} 18 | (diff #{0 1} [0 1]) 19 | => {[] (diff-elem #{0 1} [0 1])}) 20 | 21 | (behavior "strings" 22 | (assertions 23 | "simple diff" 24 | (diff "asdf" "usef") 25 | => {[] (diff-elem "asdf" "usef")})) 26 | 27 | (behavior "maps" 28 | (assertions 29 | "returns a list of paths to the diffs" 30 | (diff {0 1 1 1} {0 2}) 31 | => {[0] (diff-elem 1 2) 32 | [1] (diff-elem 1 nf)} 33 | 34 | "if actual has extra keys than expected, the diff will specify a removal" 35 | (diff {} {0 0}) 36 | => {[0] (diff-elem nf 0)} 37 | (diff {0 {}} {0 {1 1}}) 38 | => {[0 1] (diff-elem nf 1)} 39 | 40 | "nil values wont show up as removals" 41 | (diff {0 nil} {0 0}) 42 | => {[0] (diff-elem nil 0)} 43 | 44 | "recursive cases work too!" 45 | (diff {0 {1 2}} {0 {1 1}}) 46 | => {[0 1] (diff-elem 2 1)} 47 | 48 | "handles coll as keys" 49 | (diff {0 {1 {[2 3] 3 50 | {4 4} 4}}} 51 | {0 {1 {[2 3] :q}}}) 52 | => {[0 1 [2 3]] (diff-elem 3 :q) 53 | [0 1 {4 4}] (diff-elem 4 nf)} 54 | 55 | "differences accumulate over keys in map (github issue #3)" 56 | (diff {:current-tab :bookings 57 | :search-results {:global {:es-type "booking"}}} 58 | {:current-tab :messages 59 | :search-results {:global {:es-type "message"}}}) 60 | => {[:search-results :global :es-type] [:+ "booking" :- "message"] 61 | [:current-tab] [:+ :bookings :- :messages]} 62 | 63 | "empty list as key" 64 | (diff {[] :foo} {[] :bar}) 65 | => {[[]] (diff-elem :foo :bar)} 66 | 67 | "false as a key" 68 | (diff {[] {}} {[] {false 0}}) 69 | => {[[] false] (diff-elem nf 0)})) 70 | 71 | (behavior "lists" 72 | (assertions 73 | "both empty" 74 | (diff '() '()) => {} 75 | 76 | "same length" 77 | (diff '(0) '(0)) => {} 78 | (diff '(0) '(1)) => {[0] (diff-elem 0 1)} 79 | (diff '(1 2 3) '(1 3 2)) => {[1] (diff-elem 2 3) 80 | [2] (diff-elem 3 2)} 81 | 82 | "diff lengths" 83 | (diff '(4 3 1) '(4)) => {[1] (diff-elem 3 nf) 84 | [2] (diff-elem 1 nf)} 85 | (diff '() '(3 9)) => {[0] (diff-elem nf 3) 86 | [1] (diff-elem nf 9)} 87 | 88 | "diff inside a nested list" 89 | (diff '[(app/exec {})] '[(app/do-thing {})]) 90 | => {[0 0] (diff-elem 'app/exec 'app/do-thing)})) 91 | 92 | (behavior "lists & vectors" 93 | (assertions 94 | "works as though they are the same type" 95 | (diff '(0 1 3 4) [0 1 2 3]) 96 | => {[2] (diff-elem 3 2) 97 | [3] (diff-elem 4 3)})) 98 | 99 | (behavior "vectors" 100 | (assertions 101 | "both empty" 102 | (diff [] []) => {} 103 | 104 | "same length" 105 | (diff [0] [1]) 106 | => {[0] (diff-elem 0 1)} 107 | (diff [0 1] [1 2]) 108 | => {[0] (diff-elem 0 1) 109 | [1] (diff-elem 1 2)} 110 | (diff [0 1] [1 1]) 111 | => {[0] (diff-elem 0 1)} 112 | 113 | "diff lengths" 114 | (diff [] [1]) 115 | => {[0] (diff-elem nf 1)} 116 | (diff [2] []) 117 | => {[0] (diff-elem 2 nf)} 118 | (diff [] [0 1]) 119 | => {[0] (diff-elem nf 0) 120 | [1] (diff-elem nf 1)} 121 | 122 | "diff is after some equals" 123 | (diff [0 1 2 3] 124 | [0 1 2 :three]) 125 | => {[3] (diff-elem 3 :three)} 126 | 127 | "recursive!" 128 | (diff [{0 0}] [{0 1}]) 129 | => {[0 0] (diff-elem 0 1)} 130 | (diff [{:questions {:ui/curr 1}}] 131 | [{:questions {}}]) 132 | => {[0 :questions :ui/curr] (diff-elem 1 nf)})) 133 | 134 | (behavior "sets" 135 | (assertions 136 | "both empty" 137 | (diff #{} #{}) => {[] []} 138 | 139 | "only one empty" 140 | (diff #{} #{2 3 4}) => {[] (diff-elem #{} #{2 3 4})} 141 | (diff #{2 3 4} #{}) => {[] (diff-elem #{2 3 4} #{})} 142 | 143 | "same length" 144 | (diff #{1} #{2}) => {[] (diff-elem #{1} #{2})} 145 | (diff #{1 3} #{2 3}) => {[] (diff-elem #{1} #{2})} 146 | (diff #{1 5} #{2 4}) => {[] (diff-elem #{1 5} #{2 4})} 147 | 148 | "nested" 149 | (diff [{:foo [#{1 2}]}] 150 | [{:foo [#{3 2}]}]) 151 | => {[0 :foo 0] (diff-elem #{1} #{3})})) 152 | 153 | (behavior "github issue #2" 154 | (assertions 155 | (diff {:current-tab [:messages :tab] :ui/loading-data false} 156 | {:current-tab [:settings :tab] :ui/loading-data false}) 157 | => {[:current-tab 0] [:+ :messages :- :settings]}))) 158 | 159 | (specification "the patch function" 160 | (behavior "`patch` an object with a diff" 161 | (assertions 162 | (patch '() {[0] (diff-elem 'app/mut nf)}) 163 | => '(app/mut) 164 | (patch '() {[1] (diff-elem 'app/mut nf)}) 165 | =throws=> (#?(:cljs js/Error :clj IndexOutOfBoundsException) 166 | #"(?i)Index.*Out.*Of.*Bounds"))) 167 | (behavior "can patch 2 vectors, where actual is larger" 168 | (assertions 169 | (patch [] (diff [1 2] [])) => [1 2])) 170 | (behavior "gh-17, can patch a diff between a vector & a scalar" 171 | (assertions 172 | (patch [1 2] (diff 3 [1 2])) => {[] 3})) 173 | (behavior "gh-6, can patch a diff between 2 Cons or 2 IndexedSeq" 174 | (assertions 175 | (patch `[(1 2)] (diff `[(3 4)] `[(1 2)])) => `[(3 4)] 176 | (patch [1 2] (diff ((fn [& args] args) 3 4) [1 2])) => [3 4]))) 177 | 178 | (specification "[de]compression" 179 | (assertions "`compress` a sequence of states" 180 | (compress [{0 0} {0 1} {0 1 2 3}]) 181 | => [{0 0} 182 | {[0] (diff-elem 1 0)} 183 | {[2] (diff-elem 3 nf)}] 184 | 185 | (compress [{0 0} {}]) 186 | => [{0 0} {[0] (diff-elem nf 0)}]) 187 | 188 | (assertions "`decompress` a compressed sequence" 189 | (decompress [{0 0} 190 | {[0] (diff-elem 1 0)} 191 | {[2] (diff-elem 3 nf)}]) 192 | => [{0 0} {0 1} {0 1 2 3}] 193 | 194 | (decompress [{0 0} {[0] (diff-elem nf 0)}]) 195 | => [{0 0} {}])) 196 | -------------------------------------------------------------------------------- /test/untangled_spec/provided_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.provided-spec 2 | (:require 3 | [clojure.spec :as s] 4 | [untangled-spec.core #?(:clj :refer :cljs :refer-macros) 5 | [specification behavior provided assertions when-mocking]] 6 | #?(:clj [untangled-spec.impl.macros :as im]) 7 | #?(:clj [untangled-spec.provided :as p]) 8 | [untangled-spec.stub :as stub] 9 | [untangled-spec.spec :as us] 10 | [untangled-spec.testing-helpers :as th]) 11 | #?(:clj 12 | (:import clojure.lang.ExceptionInfo))) 13 | 14 | #?(:clj 15 | (specification "parse-arrow-count" 16 | (assertions 17 | "requires the arrow start with an =" 18 | (p/parse-arrow-count '->) =throws=> (AssertionError) 19 | "requires the arrow end with =>" 20 | (p/parse-arrow-count '=2x>) =throws=> (AssertionError) 21 | "derives a :many count for general arrows" 22 | (p/parse-arrow-count '=>) => :many 23 | "derives a numeric count for numbered arrows" 24 | (p/parse-arrow-count '=1x=>) => 1 25 | (p/parse-arrow-count '=7=>) => 7 26 | (p/parse-arrow-count '=234x=>) => 234))) 27 | 28 | #?(:clj 29 | (specification "parse-mock-triple" 30 | (let [test-parse (comp p/parse-mock-triple 31 | (partial us/conform! :untangled-spec.provided/triple))] 32 | (let [result (test-parse '[(f a b) =2x=> (+ a b)])] 33 | (behavior "includes a call count" 34 | (assertions 35 | (contains? result :ntimes) => true 36 | (:ntimes result) => 2)) 37 | (behavior "includes a stubbing function" 38 | (assertions 39 | (contains? result :stub-function) => true 40 | (:stub-function result) => '(clojure.core/fn [a b] (+ a b)))) 41 | (behavior "includes the symbol to mock" 42 | (assertions 43 | (contains? result :mock-name) => true 44 | (:mock-name result) => 'f))) 45 | (let [result (test-parse '[(f 1 :b c) =2x=> (+ 1 c)])] 46 | (behavior "parses literals into :literals key" 47 | (assertions 48 | (contains? result :literals) => true 49 | (:literals result) => [1 :b ::stub/any])) 50 | (behavior "converts literals in the arglist into symbols" 51 | (assertions 52 | (->> (:stub-function result) 53 | second (every? symbol?)) 54 | => true)))))) 55 | 56 | #?(:clj 57 | (specification "provided-macro" 58 | (behavior "Outputs a syntax-quoted block" 59 | (let [expanded (p/provided* false "some string" 60 | '[(f n) => (+ n 1) 61 | (f n) =2x=> (* 3 n) 62 | (under-test)])] 63 | (behavior "with a let of the scripted stubs" 64 | (let [let-defs (second (th/locate `let expanded))] 65 | (assertions 66 | (count let-defs) => 2))) 67 | (behavior "containing a script with the number proper steps" 68 | (let [script-steps (last (th/locate `stub/make-script expanded))] 69 | (assertions 70 | (vector? script-steps) => true 71 | (count script-steps) => 2 72 | (first (first script-steps)) => 'untangled-spec.stub/make-step 73 | (first (second script-steps)) => 'untangled-spec.stub/make-step))) 74 | (behavior "surrounds the assertions with a redef" 75 | (let [redef-block (th/locate `with-redefs expanded)] 76 | (assertions 77 | (first redef-block) => `with-redefs 78 | (vector? (second redef-block)) => true 79 | (th/locate 'under-test redef-block) => '(under-test)))) 80 | (behavior "sends do-report when given a string" 81 | (assertions 82 | (take 2 (th/locate `im/with-reporting expanded)) 83 | => `(im/with-reporting 84 | {:type :provided :string "PROVIDED: some string"}))))) 85 | 86 | (behavior "Can do mocking without output" 87 | (let [expanded (p/provided* false :skip-output 88 | '[(f n) => (+ n 1) 89 | (f n) =2x=> (* 3 n) 90 | (under-test)]) 91 | redef-block (th/locate `with-redefs expanded)] 92 | (assertions 93 | (first redef-block) => `with-redefs 94 | (vector? (second redef-block)) => true 95 | (th/locate 'under-test expanded) => '(under-test) 96 | "no do-report pair" 97 | (count (remove nil? redef-block)) => 4))))) 98 | 99 | (defn my-square [x] (* x x)) 100 | 101 | (defn my-varargs-sum [n & nums] (apply + n nums)) 102 | 103 | (specification "provided and when-mocking macros" 104 | (behavior "cause stubbing to work" 105 | (provided "that functions are mocked the correct number of times, with the correct output values." 106 | (my-square n) =1x=> 1 107 | (my-square n) =2x=> 1 108 | (assertions 109 | (+ (my-square 7) 110 | (my-square 7) 111 | (my-square 7)) => 3)) 112 | 113 | (behavior "throws an exception if the mock is called too many times" 114 | (when-mocking 115 | (my-square n) =1x=> (+ n 5) 116 | (my-square n) =1x=> (+ n 7) 117 | (assertions 118 | (+ (my-square 1) 119 | (my-square 1) 120 | (my-square 1)) 121 | =throws=> (ExceptionInfo)))) 122 | 123 | (provided "a mock for 3 calls with 2 different return values" 124 | (my-square n) =1x=> (+ n 5) 125 | (my-square n) =2x=> (+ n 7) 126 | (behavior "all 3 mocked calls return the mocked values" 127 | (assertions 128 | (+ (my-square 1) 129 | (my-square 1) 130 | (my-square 1)) => 22)))) 131 | 132 | (provided "we can mock a var args function" 133 | (my-varargs-sum x y) =1x=> [x y] 134 | (my-varargs-sum x y z) => [x y z] 135 | (assertions 136 | (my-varargs-sum 1 2) => [1 2] 137 | (my-varargs-sum 1 2 3) => [1 2 3])) 138 | (provided "we can capture arguments variadically" 139 | (my-varargs-sum & y) => y 140 | (assertions 141 | (my-varargs-sum 1 2 3) => [1 2 3])) 142 | 143 | (provided "allow stubs to throw exceptions" 144 | (my-square n) => (throw (ex-info "throw!" {})) 145 | (assertions 146 | (my-square 1) =throws=> (ExceptionInfo))) 147 | 148 | (behavior "allows any number of trailing forms" 149 | (let [detector (volatile! false)] 150 | (when-mocking 151 | (my-square n) =1x=> (+ n 5) 152 | (my-square n) => (+ n 7) 153 | 154 | (+ 1 2) (+ 1 2) (+ 1 2) (+ 1 2) 155 | (* 3 3) (* 3 3) (* 3 3) (* 3 3) 156 | (my-square 2) 157 | (my-square 2) 158 | (vreset! detector true)) 159 | (assertions 160 | @detector => true)))) 161 | -------------------------------------------------------------------------------- /test/untangled_spec/selectors_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.selectors-spec 2 | (:require 3 | [untangled-spec.core :refer [specification behavior component assertions]] 4 | [untangled-spec.selectors :as sel])) 5 | 6 | (defn longform [active & [inactive]] 7 | (vec 8 | (concat 9 | (map #(hash-map :selector/id % :selector/active? true) active) 10 | (map #(hash-map :selector/id % :selector/active? false) inactive)))) 11 | 12 | (specification "selectors" :focused 13 | (component "set-selectors" 14 | (assertions 15 | (sel/set-selectors* (longform #{:a :b :focused}) 16 | #{:focused}) 17 | => (longform #{:focused} #{:a :b}))) 18 | 19 | (component "selected-for?" 20 | (assertions 21 | "active selectors only apply on tests that have the selector" 22 | (sel/selected-for?* (longform #{:focused}) #{}) => false 23 | (sel/selected-for?* (longform #{:focused}) nil) => false 24 | (sel/selected-for?* (longform #{:focused}) #{:focused}) => true 25 | 26 | "selected if it's an active selector or not defined" 27 | (sel/selected-for?* (longform #{}) #{:focused}) => true 28 | (sel/selected-for?* (longform #{} #{:focused}) #{:focused}) => false 29 | (sel/selected-for?* (longform #{:focused}) #{:focused}) => true 30 | (sel/selected-for?* (longform #{:focused}) #{:asdf}) => true 31 | (sel/selected-for?* (longform #{:focused} #{:asdf}) #{:asdf}) => false 32 | 33 | "must pass at least one active selector" 34 | (sel/selected-for?* (longform #{:unit :focused}) #{:focused}) => true 35 | (sel/selected-for?* (longform #{:unit :focused}) #{:qa}) => true 36 | (sel/selected-for?* (longform #{:unit :focused} #{:qa}) #{:qa}) => false))) 37 | -------------------------------------------------------------------------------- /test/untangled_spec/stub_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.stub-spec 2 | (:require 3 | [untangled-spec.stub :as s 4 | #?@(:cljs [:include-macros true])] 5 | [untangled-spec.core #?(:clj :refer :cljs :refer-macros) 6 | [specification behavior provided assertions]] 7 | #?(:clj [clojure.test :refer [is]]) 8 | #?(:cljs [cljs.test :refer-macros [is]])) 9 | #?(:clj 10 | (:import clojure.lang.ExceptionInfo))) 11 | 12 | (defn make-simple-script [] 13 | (s/make-script "something" 14 | [(s/make-step 'stub 1 [])])) 15 | 16 | (specification "increment-script-call-count" 17 | (behavior "finds and increments the correct step" 18 | (let [script (make-simple-script)] 19 | 20 | (s/increment-script-call-count script 0) 21 | 22 | (is (= 1 (get-in @script [:steps 0 :times])))))) 23 | 24 | (specification "step-complete" 25 | (let [script (make-simple-script)] 26 | (behavior "is false when call count is less than expected count" 27 | (is (not (s/step-complete script 0)))) 28 | (s/increment-script-call-count script 0) 29 | (behavior "is true when call count reaches expected count" 30 | (is (s/step-complete script 0))))) 31 | 32 | (defn make-call-script [to-call & {:keys [literals N] 33 | :or {N 1, literals []}}] 34 | (s/scripted-stub 35 | (s/make-script "something" 36 | [(s/make-step to-call N literals)]))) 37 | 38 | (specification "scripted-stub" 39 | (behavior "calls the stub function" 40 | (let [detector (atom false) 41 | sstub (make-call-script (fn [] (reset! detector true)))] 42 | (sstub), (is (= true @detector)))) 43 | 44 | (behavior "verifies the stub fn is called with the correct literals" 45 | (let [sstub (make-call-script 46 | (fn [n x] [(inc n) x]) 47 | :literals [41 ::s/any] 48 | :N :many)] 49 | (assertions 50 | (sstub 41 :foo) => [42 :foo] 51 | (sstub 2 :whatever) =throws=> (ExceptionInfo #"called with wrong arguments") 52 | (try (sstub 2 :evil) 53 | (catch ExceptionInfo e (ex-data e))) 54 | => {:args [2 :evil] 55 | :expected-literals [41 ::s/any]}))) 56 | 57 | (behavior "returns whatever the stub function returns" 58 | (let [sstub (make-call-script (fn [] 42))] 59 | (assertions (sstub) => 42))) 60 | 61 | (behavior "throws an exception if the function is invoked more than programmed" 62 | (let [sstub (make-call-script (fn [] 42))] 63 | (sstub) ; first call 64 | (assertions 65 | (try (sstub 1 2 3) (catch ExceptionInfo e (ex-data e))) 66 | => {:max-calls 1 67 | :args '(1 2 3)}))) 68 | 69 | (behavior "throws whatever exception the function throws" 70 | (let [sstub (make-call-script (fn [] (throw (ex-info "BUMMER" {}))))] 71 | (assertions 72 | (sstub) =throws=> (ExceptionInfo)))) 73 | 74 | (behavior "only moves to the next script step if the call count for the current step reaches the programmed amount" 75 | (let [a-count (atom 0) 76 | b-count (atom 0) 77 | script (s/make-script "something" 78 | [(s/make-step (fn [] (swap! a-count inc)) 2 []) 79 | (s/make-step (fn [] (swap! b-count inc)) 1 nil)]) 80 | sstub (s/scripted-stub script)] 81 | (assertions 82 | (repeatedly 3 (fn [] (sstub) [@a-count @b-count])) 83 | => [[1 0] [2 0] [2 1]]))) 84 | 85 | (behavior "records the call argument history" 86 | (let [script (s/make-script "something" 87 | [(s/make-step (fn [& args] args) 2 nil) 88 | (s/make-step (fn [& args] args) 1 nil)]) 89 | sstub (s/scripted-stub script)] 90 | (sstub 1 2) (sstub 3 4), (sstub :a :b) 91 | (assertions 92 | (:history @script) => [[1 2] [3 4] [:a :b]] 93 | (map :history (:steps @script)) => [[[1 2] [3 4]] [[:a :b]]])))) 94 | 95 | (specification "validate-target-function-counts" 96 | (behavior "returns nil if a target function has been called enough times" 97 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 5 :times 5}]})]] 98 | (assertions 99 | (s/validate-target-function-counts script-atoms) 100 | =fn=> some?))) 101 | (behavior "throws an exception when a target function has not been called enough times" 102 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 0 :times 5}]})]] 103 | (assertions 104 | (s/validate-target-function-counts script-atoms) 105 | =throws=> (ExceptionInfo)))) 106 | 107 | (behavior "returns nil if a target function has been called enough times with :many specified" 108 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 1 :times :many}]})]] 109 | (assertions 110 | (s/validate-target-function-counts script-atoms) 111 | =fn=> some?))) 112 | 113 | (behavior "throws an exception if a function has not been called at all with :many was specified" 114 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 0 :times :many}]})]] 115 | (assertions 116 | (s/validate-target-function-counts script-atoms) 117 | =throws=> (ExceptionInfo)))) 118 | 119 | (behavior "returns nil all the function have been called the specified number of times" 120 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 1 :times 1}]}) 121 | (atom {:function "fun2" :steps [{:ncalled 1 :times 1}]})]] 122 | (assertions 123 | (s/validate-target-function-counts script-atoms) 124 | =fn=> some?))) 125 | 126 | (behavior "throws an exception if the second function has not been called at all with :many was specified" 127 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 1 :times 1}]}) 128 | (atom {:function "fun2" :steps [{:ncalled 0 :times 1}]})]] 129 | (assertions 130 | (s/validate-target-function-counts script-atoms) 131 | =throws=> (ExceptionInfo)))) 132 | 133 | (behavior "stubs record history, will show the script when it fails to validate" 134 | (let [script-atoms [(atom {:function "fun1" :steps [{:ncalled 1 :times 1}]}) 135 | (atom {:function "fun2" :steps [{:ncalled 1 :times 2}]})]] 136 | (assertions 137 | (s/validate-target-function-counts script-atoms) 138 | =throws=> (ExceptionInfo #"" 139 | #(assertions 140 | (ex-data %) => {:function "fun2" 141 | :steps [{:ncalled 1 :times 2}]})))))) 142 | -------------------------------------------------------------------------------- /test/untangled_spec/testing_helpers.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.testing-helpers 2 | (:require 3 | [clojure.walk :as w])) 4 | 5 | (defn locate 6 | "Locates and returns (sym ...) in haystack. 7 | For use in tests that assert against code blocks for assertions 8 | about content that are decoupled from shape and/or location." 9 | [sym haystack] 10 | (let [needle (volatile! nil)] 11 | (w/prewalk 12 | #(do (when (and (not @needle) 13 | (seq? %) (= sym (first %))) 14 | (vreset! needle %)) 15 | %) 16 | haystack) 17 | @needle)) 18 | -------------------------------------------------------------------------------- /test/untangled_spec/tests_to_run.cljs: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.tests-to-run 2 | (:require 3 | untangled-spec.assertions-spec 4 | untangled-spec.async-spec 5 | untangled-spec.contains-spec 6 | untangled-spec.diff-spec 7 | untangled-spec.provided-spec 8 | untangled-spec.selectors-spec 9 | untangled-spec.stub-spec 10 | untangled-spec.timeline-spec)) 11 | 12 | ;******************************************************************************** 13 | ; IMPORTANT: 14 | ; For cljs tests to work in CI, we want to ensure the namespaces for all tests are included/required. By placing them 15 | ; here (and depending on them in user.cljs for dev), we ensure that the all-tests namespace (used by CI) loads 16 | ; everything as well. 17 | ;******************************************************************************** 18 | -------------------------------------------------------------------------------- /test/untangled_spec/timeline_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns untangled-spec.timeline-spec 2 | (:require [untangled-spec.core #?(:clj :refer :cljs :refer-macros) 3 | [specification behavior provided 4 | with-timeline async tick assertions]] 5 | #?(:clj [clojure.test :refer [is]]) 6 | #?(:cljs [cljs.test :refer-macros [is]]))) 7 | 8 | 9 | #?(:cljs 10 | (specification "Timeline" 11 | (behavior "within a timeline" 12 | (with-timeline 13 | (let [detector (atom [])] 14 | (provided "when mocking setTimeout" 15 | (js/setTimeout f n) =2x=> (async n (f)) 16 | 17 | (js/setTimeout (fn [] (js/setTimeout (fn [] (swap! detector conj "LAST")) 300) (swap! detector conj "FIRST")) 100) 18 | 19 | (behavior "nothing called until timer moves past first specified event is to occur" 20 | (is (= 0 (count @detector))) 21 | ) 22 | 23 | (behavior "after first tick only the callbacks that satisfy the" 24 | (tick 101) 25 | (is (= 1 (count @detector))) 26 | ) 27 | 28 | (behavior "more functions can run before next callback is called" 29 | (swap! detector conj "SECOND") 30 | (is (= 2 (count @detector))) 31 | ) 32 | 33 | (behavior "after all time is passed all callback timers are fired" 34 | (tick 301) 35 | (is (= 3 (count @detector))) 36 | (is (= "FIRST" (first @detector))) 37 | (is (= "SECOND" (second @detector))) 38 | (is (= "LAST" (last @detector))) 39 | ) 40 | ) 41 | ) 42 | ) 43 | ) 44 | ) 45 | ) 46 | --------------------------------------------------------------------------------