├── .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 |
--------------------------------------------------------------------------------