Skip to content

Commit

Permalink
Add malli schema for resolved-info and validate it at runtime
Browse files Browse the repository at this point in the history
- Use `cljest__is` matcher directly rather than `cljest.core/is-matcher` in `is` macro
  • Loading branch information
jo-sm committed May 26, 2023
1 parent 74979de commit 7469979
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 35 deletions.
58 changes: 39 additions & 19 deletions cljest/src/cljest/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[cljest.format :as format]
[cljs.analyzer.api :as analyzer.api]
cljs.env
[clojure.string :as str]))
[malli.core :as malli]))

(def ^:private user-defined-formatters-ns (some-> (config/get-config!)
(get :formatters-ns)
Expand Down Expand Up @@ -126,29 +126,49 @@
(doseq ~seq-exprs (only ~name ~@body))
js/undefined))

;; TODO: spec
(defn ^:private value->resolved-info
[env value]
(if (symbol? value)
(let [resolved (analyzer.api/resolve env value)
matcher-name (get-in resolved [:meta :jest-matcher])]
(if matcher-name
{:value value
:type :matcher
:matcher-name matcher-name}
{:value value
:type :symbol
:resolved (get resolved :name 'unknown)}))
{:value value
:type :primitive
:resolved 'primitive}))
(def ^:private matcher-resolved-info [:map
{:closed true}
[:type [:enum :matcher]]
[:value :any]
[:matcher-name :string]])
(def ^:private non-matcher-resolved-info [:map
{:closed true}
[:type [:enum :symbol :primitive]]
[:value :any]
[:resolved :symbol]])
(def ^:private resolved-info [:multi {:dispatch :type}
[:matcher matcher-resolved-info]
[:symbol non-matcher-resolved-info]
[:primitive non-matcher-resolved-info]])

;; TODO: instrument using something like `malli.instrument/instrument!`
;; so we can just use `defn`
(def ^:private value->resolved-info
(malli/-instrument
{:schema [:=> [:cat :map :any] resolved-info]}
(fn [env value]
(if (symbol? value)
(let [resolved (analyzer.api/resolve env value)
matcher-name (get-in resolved [:meta :jest-matcher])]
(if matcher-name
{:value value
:type :matcher
:matcher-name matcher-name}
{:value value
:type :symbol
:resolved (get resolved :name (symbol 'unknown))}))
{:value value
:type :primitive
:resolved (symbol 'primitive)}))))

(defmacro ^:private primitive-is
"The form of `is` used when the value is primitive, i.e. not a sequence."
[form negated?]
(let [{:keys [resolved]} (value->resolved-info &env form)]
`(cljest.core/is-matcher #(do ~form) ~(format/formatter resolved form negated?))))
`(.. (js/expect #(do ~form)) ~'-cljest__is (~'call nil ~(format/formatter resolved form negated?)))))

(defmacro ^:private matcher-is
"The form of `is` used when the value is a Jest matcher."
[matcher-name body negated?]
(let [args (rest body)
asserted-value (first args)
Expand All @@ -171,7 +191,7 @@

;; For the actual assertion, we want the full body, but for the formatter, we want to pass the possibly inner part
;; of (not (...)) to simplify writing the macro.
`(cljest.core/is-matcher #(do ~forms) ~(format/formatter resolved body negated?)))))
`(.. (js/expect #(do ~forms)) ~'-cljest__is (~'call nil ~(format/formatter resolved body negated?))))))

(defmacro is
"A generic assertion macro for Jest. Asserts that `form` is truthy.
Expand Down
7 changes: 0 additions & 7 deletions cljest/src/cljest/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,3 @@
(spy-on object method-name js/undefined))
([object method-name access-type]
(.spyOn jest object method-name access-type)))

(defn is-matcher
"The underlying matcher for `is`.
Don't use this directly, use the `cljest.core/is` macro."
[body-fn formatter]
(.. (js/expect body-fn) (cljest__is formatter)))
22 changes: 13 additions & 9 deletions cljest/src/cljest/core_test.cljs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns cljest.core-test
(:require [cljest.core :refer [describe is it]]
[cljest.helpers.dom :as h.dom]
[cljest.matchers :as m]
[cyrik.cljs-macroexpand :refer [cljs-macroexpand-all] :rename {cljs-macroexpand-all macroexpand-all}]))
[cyrik.cljs-macroexpand :refer [cljs-macroexpand-all] :rename {cljs-macroexpand-all macroexpand-all}]
[malli.core :as malli]))

(describe "is"
(it "should support non-list forms (primitives)"
Expand All @@ -25,17 +25,21 @@
(is @ex)))

(it "should create an `is` expect call when expanded with a complex value"
;; Because the third value in the sequence is a function that we don't really care about, we only really
;; want to assert against the first two arguments. It's a bit cumbersome but I couldn't find a better way
;; to do this... If someone sees this and knows a better way to do it, please create a PR!
(let [expanded (macroexpand-all '(is (= 3 (+ 1 2))))]
(is (= 'cljest.core/is-matcher (first expanded)))
(is (= (macroexpand-all '#(do (= 3 (+ 1 2)))) (second expanded)))))

(is (= (macroexpand-all '(. (js/expect #(do (= 3 (+ 1 2)))) -cljest__is)) (nth expanded 1)))

;; While it's a bit clunky to validate with `:tuple`, we want to assert that we have a symbol
;; (call), nil, and then a sequence, which is the formatter function. I tried to use `match` but
;; couldn't get it to work.
;;
;; Suggestions for improving this assertion are welcome!
(is (malli/validate [:tuple symbol? nil? seq?] (into [] (nth expanded 2))))))

(it "should create an expect call when expanded with a primitive value"
(let [expanded (macroexpand-all '(is true))]
(is (= 'cljest.core/is-matcher (first expanded)))
(is (= (macroexpand-all '#(do true)) (second expanded)))))
(is (= (macroexpand-all '(. (js/expect #(do true)) -cljest__is)) (nth expanded 1)))
(is (malli/validate [:tuple symbol? nil? seq?] (into [] (nth expanded 2))))))

(it "should create only one expect call if called with a matcher"
(is (= (macroexpand-all '(is (m/visible? (h.dom/get-by :text "hello"))))
Expand Down

0 comments on commit 7469979

Please sign in to comment.