Merge pull request #183 from samroberton/cljc

Converting to CLJC for JVM testing
This commit is contained in:
Sam Roberton 2016-07-22 09:59:07 +10:00 committed by GitHub
commit a169e1bc5e
14 changed files with 217 additions and 94 deletions

View File

@ -1,4 +1,4 @@
(defproject re-frame "0.8.0-alpha2" (defproject re-frame "0.8.0-alpha3-SNAPSHOT"
:description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
:url "https://github.com/Day8/re-frame.git" :url "https://github.com/Day8/re-frame.git"
:license {:name "MIT"} :license {:name "MIT"}
@ -7,12 +7,13 @@
[reagent "0.6.0-rc"]] [reagent "0.6.0-rc"]]
:profiles {:debug {:debug true} :profiles {:debug {:debug true}
:dev {:dependencies [[karma-reporter "0.3.0"] :dev {:dependencies [[karma-reporter "0.3.0"]
[binaryage/devtools "0.7.2"]] [binaryage/devtools "0.7.2"]
:plugins [[lein-cljsbuild "1.1.3"] [org.clojure/tools.logging "0.3.1"]]
[lein-npm "0.6.2"] :plugins [[lein-cljsbuild "1.1.3"]
[lein-figwheel "0.5.4-7"] [lein-npm "0.6.2"]
[lein-shell "0.5.0"]]}} [lein-figwheel "0.5.4-7"]
[lein-shell "0.5.0"]]}}
:clean-targets [:target-path "run/compiled"] :clean-targets [:target-path "run/compiled"]

View File

@ -1,5 +1,5 @@
(ns re-frame.db (ns re-frame.db
(:require [reagent.core :as reagent])) (:require [re-frame.interop :refer [ratom]]))
;; -- Application State -------------------------------------------------------------------------- ;; -- Application State --------------------------------------------------------------------------
@ -7,5 +7,5 @@
;; Should not be accessed directly by application code ;; Should not be accessed directly by application code
;; Read access goes through subscriptions. ;; Read access goes through subscriptions.
;; Updates via event handlers. ;; Updates via event handlers.
(def app-db (reagent/atom {})) (def app-db (ratom {}))

View File

@ -103,7 +103,7 @@
:stack)] :stack)]
(try (try
(handler-fn app-db event-v) (handler-fn app-db event-v)
(catch :default e (catch #?(:cljs :default :clj Exception) e
(console :warn stack) ;; output a msg to help to track down dispatching point (console :warn stack) ;; output a msg to help to track down dispatching point
(throw e))))))))) (throw e)))))))))

View File

@ -1,8 +1,8 @@
(ns re-frame.fx (ns re-frame.fx
(:require [reagent.ratom :refer [IReactiveAtom]] (:require [re-frame.router :refer [dispatch]]
[re-frame.router :refer [dispatch]]
[re-frame.db :refer [app-db]] [re-frame.db :refer [app-db]]
[re-frame.events] [re-frame.events]
[re-frame.interop :refer [ratom? set-timeout!]]
[re-frame.loggers :refer [console]])) [re-frame.loggers :refer [console]]))
@ -54,7 +54,7 @@
:dispatch-later :dispatch-later
(fn [effect] (fn [effect]
(doseq [[ms events] effect] (doseq [[ms events] effect]
(js/setTimeout #(dispatch-helper events) ms)))) (set-timeout! #(dispatch-helper events) ms))))
;; Supply either a vector or a list of vectors. For example: ;; Supply either a vector or a list of vectors. For example:
@ -120,7 +120,7 @@
[handler] [handler]
(fn fx-handler (fn fx-handler
[app-db event-vec] [app-db event-vec]
(if-not (satisfies? IReactiveAtom app-db) (if-not (ratom? app-db)
(if (map? app-db) (if (map? app-db)
(console :warn "re-frame: Did you use \"fx\" middleware with \"def-event\"? Use \"def-event-fx\" instead (and don't directly use \"fx\")") (console :warn "re-frame: Did you use \"fx\" middleware with \"def-event\"? Use \"def-event-fx\" instead (and don't directly use \"fx\")")
(console :warn "re-frame: \"fx\" middleware not given a Ratom. Got: " app-db))) (console :warn "re-frame: \"fx\" middleware not given a Ratom. Got: " app-db)))

64
src/re_frame/interop.clj Normal file
View File

@ -0,0 +1,64 @@
(ns re-frame.interop
(:import java.util.concurrent.Executors))
;; The purpose of this file is to provide JVM-runnable implementations of the
;; CLJS equivalents in interop.cljs.
;;
;; These implementations are to enable you to bring up a re-frame app on the JVM
;; in order to run tests, or to develop at a JVM REPL instead of a CLJS one.
;;
;; Please note, though, that the purpose here *isn't* to fully replicate all of
;; re-frame's behaviour in a real CLJS environment. We don't have Reagent or
;; React on the JVM, and we don't try to mimic the stateful lifecycles that they
;; embody.
;;
;; In particular, if you're performing side effects in any code that's triggered
;; by a change to a Ratom's value, and not via a call to `dispatch`, then you're
;; going to have a hard time getting any accurate tests with this code.
;; However, if your subscriptions and Reagent render functions are pure, and
;; your side-effects are all managed by event handlers, then hopefully this will
;; allow you to write some useful tests that can run on the JVM.
(defonce ^:private executor (Executors/newSingleThreadExecutor))
(defn next-tick [f]
(.execute executor f)
nil)
(def empty-queue clojure.lang.PersistentQueue/EMPTY)
(def after-render next-tick)
(def debug-enabled? true)
(defn ratom [x]
(atom x))
(defn ratom? [x]
(instance? clojure.lang.IAtom x))
(defn make-reaction
"On JVM Clojure, return a `deref`-able thing which invokes the given function
on every `deref`. That is, `make-reaction` here provides precisely none of the
benefits of `reagent.ratom/make-reaction` (which only invokes its function if
the reactions that the function derefs have changed value). But so long as `f`
only depends on other reactions (which also behave themselves), the only
difference is one of efficiency. That is, your tests should see no difference
other than that they do redundant work."
[f]
(reify clojure.lang.IDeref
(deref [_] (f))))
(defn add-on-dispose!
"No-op in JVM Clojure, since for testing purposes, we don't care about
releasing resources for efficiency purposes."
[a-ratom f]
nil)
(defn set-timeout!
"Note that we ignore the `ms` value and just invoke the function, because
there isn't often much point firing a timed event in a test."
[f ms]
(next-tick f))

27
src/re_frame/interop.cljs Normal file
View File

@ -0,0 +1,27 @@
(ns re-frame.interop
(:require [goog.async.nextTick]
[reagent.core]
[reagent.ratom]))
(def next-tick goog.async.nextTick)
(def empty-queue #queue [])
(def after-render reagent.core/after-render)
(def ^:boolean debug-enabled? js/goog.DEBUG)
(defn ratom [x]
(reagent.core/atom x))
(defn ratom? [x]
(satisfies? reagent.ratom/IReactiveAtom x))
(defn make-reaction [f]
(reagent.ratom/make-reaction f))
(defn add-on-dispose! [a-ratom f]
(reagent.ratom/add-on-dispose! a-ratom f))
(defn set-timeout! [f ms]
(js/setTimeout f ms))

43
src/re_frame/loggers.cljc Normal file
View File

@ -0,0 +1,43 @@
(ns re-frame.loggers
(:require
[clojure.set :refer [difference]]
#?@(:clj [[clojure.string :as str]
[clojure.tools.logging :as log]])))
(defn log [level & args]
(log/log level (if (= 1 (count args))
(first args)
(str/join " " (map pr-str args)))))
;; "loggers" holds the current set of logging functions.
;; By default, re-frame uses the functions provided by js/console.
;; Use set-loggers! to change these defaults.
(def ^:private loggers
(atom {:log #?(:cljs (js/console.log.bind js/console)
:clj (partial log :info))
:warn #?(:cljs (js/console.warn.bind js/console)
:clj (partial log :warn))
:error #?(:cljs (js/console.error.bind js/console)
:clj (partial log :error))
:group #?(:cljs (if (.-group js/console) ;; console.group does not exist < IE 11
(js/console.group.bind js/console)
(js/console.log.bind js/console))
:clj (partial log :info))
:groupEnd #?(:cljs (if (.-groupEnd js/console) ;; console.groupEnd does not exist < IE 11
(js/console.groupEnd.bind js/console)
#())
:clj #())}))
(defn console
[level & args]
(assert (contains? @loggers level) (str "re-frame: log called with unknown level: " level))
(apply (level @loggers) args))
(defn set-loggers!
"Change the set (or a subset) of logging functions used by re-frame.
'new-loggers' should be a map which looks like default-loggers"
[new-loggers]
(assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers")
(swap! loggers merge new-loggers))

View File

@ -1,27 +0,0 @@
(ns re-frame.loggers
(:require
[clojure.set :refer [difference]]))
;; "loggers" holds the current set of logging functions.
;; By default, re-frame uses the functions provided by js/console.
;; Use set-loggers! to change these defaults.
(def ^:private loggers
(atom {:log (js/console.log.bind js/console)
:warn (js/console.warn.bind js/console)
:error (js/console.error.bind js/console)
:group (if (.-group js/console) (js/console.group.bind js/console) (js/console.log.bind js/console)) ;; console.group does not exist < IE 11
:groupEnd (if (.-groupEnd js/console) (js/console.groupEnd.bind js/console) #())})) ;; console.groupEnd does not exist < IE 11
(defn console
[level & args]
(assert (contains? @loggers level) (str "re-frame: log called with unknown level: " level))
(apply (level @loggers) args))
(defn set-loggers!
"Change the set (or a subset) of logging functions used by re-frame.
'new-loggers' should be a map which looks like default-loggers"
[new-loggers]
(assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers")
(swap! loggers merge new-loggers))

View File

@ -1,6 +1,6 @@
(ns re-frame.middleware (ns re-frame.middleware
(:require (:require
[reagent.ratom :refer [IReactiveAtom]] [re-frame.interop :refer [ratom?]]
[re-frame.loggers :refer [console]] [re-frame.loggers :refer [console]]
[clojure.data :as data])) [clojure.data :as data]))
@ -22,7 +22,7 @@
[handler] [handler]
(fn pure-handler (fn pure-handler
[app-db event-vec] [app-db event-vec]
(if-not (satisfies? IReactiveAtom app-db) (if-not (ratom? app-db)
(do (do
(if (map? app-db) (if (map? app-db)
(console :warn "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.") (console :warn "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.")

View File

@ -1,7 +1,6 @@
(ns re-frame.router (ns re-frame.router
(:require [reagent.core] (:require [re-frame.events :refer [handle]]
[re-frame.events :refer [handle]] [re-frame.interop :refer [after-render empty-queue next-tick]]))
[goog.async.nextTick]))
;; -- Router Loop ------------------------------------------------------------ ;; -- Router Loop ------------------------------------------------------------
@ -61,8 +60,8 @@
;; Events can have metadata which says to pause event processing. ;; Events can have metadata which says to pause event processing.
;; event metadata -> "run later" functions ;; event metadata -> "run later" functions
(def later-fns (def later-fns
{:flush-dom (fn [f] ((.-after-render reagent.core) #(goog.async.nextTick f))) ;; one tick after the end of the next annimation frame {:flush-dom (fn [f] (after-render #(next-tick f))) ;; one tick after the end of the next annimation frame
:yield goog.async.nextTick}) ;; almost immediately :yield next-tick}) ;; almost immediately
;; Abstract representation of the Event Queue ;; Abstract representation of the Event Queue
@ -88,9 +87,9 @@
;; Concrete implementation of IEventQueue ;; Concrete implementation of IEventQueue
(deftype EventQueue [^:mutable fsm-state (deftype EventQueue [#?(:cljs ^:mutable fsm-state :clj ^:volatile-mutable fsm-state)
^:mutable queue #?(:cljs ^:mutable queue :clj ^:volatile-mutable queue)
^:mutable post-event-callback-fns] #?(:cljs ^:mutable post-event-callback-fns :clj ^:volatile-mutable post-event-callback-fns)]
IEventQueue IEventQueue
;; -- API ------------------------------------------------------------------ ;; -- API ------------------------------------------------------------------
@ -144,7 +143,8 @@
[:paused :add-event] [:paused #(-add-event this arg)] [:paused :add-event] [:paused #(-add-event this arg)]
[:paused :resume ] [:running #(-resume this)] [:paused :resume ] [:running #(-resume this)]
(throw (js/Error. (str "re-frame: router state transition not found. " fsm-state " " trigger))))] (throw (ex-info (str "re-frame: router state transition not found. " fsm-state " " trigger)
{:fsm-state fsm-state, :trigger trigger})))]
;; The "case" above computed both the new FSM state, and the action. Now, make it happen. ;; The "case" above computed both the new FSM state, and the action. Now, make it happen.
(set! fsm-state new-fsm-state) (set! fsm-state new-fsm-state)
@ -161,12 +161,12 @@
(handle event-v) (handle event-v)
(set! queue (pop queue)) (set! queue (pop queue))
(-call-post-event-callbacks this event-v) (-call-post-event-callbacks this event-v)
(catch :default ex (catch #?(:cljs :default :clj Exception) ex
(-fsm-trigger this :exception ex))))) (-fsm-trigger this :exception ex)))))
(-run-next-tick (-run-next-tick
[this] [this]
(goog.async.nextTick #(-fsm-trigger this :run-queue nil))) (next-tick #(-fsm-trigger this :run-queue nil)))
;; Process all the events currently in the queue, but not any new ones. ;; Process all the events currently in the queue, but not any new ones.
;; Be aware that events might have metadata which will pause processing. ;; Be aware that events might have metadata which will pause processing.
@ -182,7 +182,7 @@
(-exception (-exception
[_ ex] [_ ex]
(set! queue #queue []) ;; purge the queue (set! queue empty-queue) ;; purge the queue
(throw ex)) (throw ex))
(-pause (-pause
@ -206,7 +206,7 @@
;; When "dispatch" is called, the event is added into this event queue. Later, ;; When "dispatch" is called, the event is added into this event queue. Later,
;; the queue will "run" and the event will be "handled" by the registered function. ;; the queue will "run" and the event will be "handled" by the registered function.
;; ;;
(def event-queue (->EventQueue :idle #queue [] [])) (def event-queue (->EventQueue :idle empty-queue []))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@ -224,11 +224,12 @@
;; anything goes wrong, we know where it came from. ;; anything goes wrong, we know where it came from.
;; To get a source mapped stack, we must get rid of the react frames ;; To get a source mapped stack, we must get rid of the react frames
;; See https://github.com/Day8/re-frame/issues/164#issuecomment-233528154 ;; See https://github.com/Day8/re-frame/issues/164#issuecomment-233528154
stack (->> (js/Error. (str "Event " (first event-v) " dispatched from here:")) stack #?(:cljs (->> (js/Error. (str "Event " (first event-v) " dispatched from here:"))
.-stack .-stack
clojure.string/split-lines clojure.string/split-lines
(remove #(re-find #"react.inc.js|\(native\)" %)) (remove #(re-find #"react\.inc\.js|\(native\)" %))
(clojure.string/join "\n"))] (clojure.string/join "\n"))
:clj "n/a")]
(if (nil? event-v) (if (nil? event-v)
(throw (ex-info "re-frame: you called \"dispatch\" without an event vector." {})) (throw (ex-info "re-frame: you called \"dispatch\" without an event vector." {}))
(push event-queue (with-meta event-v {:stack stack})))) (push event-queue (with-meta event-v {:stack stack}))))

View File

@ -1,7 +1,7 @@
(ns re-frame.subs (ns re-frame.subs
(:require (:require
[reagent.ratom :as ratom :refer [make-reaction] :refer-macros [reaction]]
[re-frame.db :refer [app-db]] [re-frame.db :refer [app-db]]
[re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom?]]
[re-frame.loggers :refer [console]] [re-frame.loggers :refer [console]]
[re-frame.utils :refer [first-in-vector]])) [re-frame.utils :refer [first-in-vector]]))
@ -38,8 +38,8 @@
[query-v dynv r] [query-v dynv r]
(let [cache-key [query-v dynv]] (let [cache-key [query-v dynv]]
;; when this reaction is nolonger being used, remove it from the cache ;; when this reaction is nolonger being used, remove it from the cache
(ratom/add-on-dispose! r #(do (swap! query->reaction dissoc cache-key) (add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
(console :warn "Removing subscription: " cache-key))) (console :warn "Removing subscription: " cache-key)))
(console :log "Dispatch site: ") (console :log "Dispatch site: ")
(console :log (:dispatch-site (meta query-v))) (console :log (:dispatch-site (meta query-v)))
@ -77,17 +77,17 @@
cached) cached)
(let [query-id (first-in-vector v) (let [query-id (first-in-vector v)
handler-fn (get @qid->fn query-id)] handler-fn (get @qid->fn query-id)]
(when ^boolean js/goog.DEBUG (when debug-enabled?
(when-let [not-reactive (remove #(implements? reagent.ratom/IReactiveAtom %) dynv)] (when-let [not-reactive (remove ratom? dynv)]
(console :warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom: " not-reactive))) (console :warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom: " not-reactive)))
(if (nil? handler-fn) (if (nil? handler-fn)
(console :error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.") (console :error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")
(let [dyn-vals (reaction (mapv deref dynv)) (let [dyn-vals (make-reaction (fn [] (mapv deref dynv)))
sub (reaction (handler-fn app-db v @dyn-vals))] sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
;; handler-fn returns a reaction which is then wrapped in the sub reaction ;; handler-fn returns a reaction which is then wrapped in the sub reaction
;; need to double deref it to get to the actual value. ;; need to double deref it to get to the actual value.
(console :warn "Subscription created: " v dynv) (console :warn "Subscription created: " v dynv)
(cache-and-return v dynv (reaction @@sub)))))))) (cache-and-return v dynv (make-reaction (fn [] @@sub)))))))))
;; -- Helper code for register-pure ------------------- ;; -- Helper code for register-pure -------------------
@ -154,22 +154,37 @@
sub-fn ;; first case the user provides a custom sub-fn sub-fn ;; first case the user provides a custom sub-fn
(register (register
sub-name sub-name
(fn [db q-vec d-vec] (fn subs-handler-fn ;; multi-arity to match the arities `subscribe` might invoke.
(let [subscriptions (sub-fn q-vec d-vec)] ;; this let needs to be outside the fn ([db q-vec]
(ratom/make-reaction (let [subscriptions (sub-fn q-vec)]
(fn [] (f (multi-deref subscriptions) q-vec d-vec)))))) (make-reaction
(fn [] (f (multi-deref subscriptions) q-vec)))))
([db q-vec d-vec]
(let [subscriptions (sub-fn q-vec d-vec)]
(make-reaction
(fn [] (f (multi-deref subscriptions) q-vec d-vec)))))))
(seq arrow-args) ;; the user uses the :<- sugar (seq arrow-args) ;; the user uses the :<- sugar
(register (register
sub-name sub-name
(fn [db q-vec d-vec] (letfn [(get-subscriptions []
(let [subscriptions (map subscribe arrow-subs) (let [subscriptions (map subscribe arrow-subs)]
subscriptions (if (< 1 (count subscriptions)) (if (< 1 (count subscriptions))
subscriptions subscriptions
(first subscriptions))] ;; automatically provide a singlton (first subscriptions))))] ;; automatically provide a singleton
(ratom/make-reaction (fn subs-handler-fn
(fn [] (f (multi-deref subscriptions) q-vec d-vec)))))) ([db q-vec]
(let [subscriptions (get-subscriptions)]
(make-reaction
(fn [] (f (multi-deref subscriptions) q-vec)))))
([db q-vec d-vec]
(let [subscriptions (get-subscriptions)]
(make-reaction
(fn [] (f (multi-deref subscriptions) q-vec d-vec))))))))
:else :else
(register ;; the simple case with no subs (register ;; the simple case with no subs
sub-name sub-name
(fn [db q-vec d-vec] (fn subs-handler-fn
(ratom/make-reaction (fn [] (f @db q-vec d-vec)))))))()) ([db q-vec]
(make-reaction (fn [] (f @db q-vec))))
([db q-vec d-vec]
(make-reaction (fn [] (f @db q-vec d-vec))))))))())

View File

@ -1,10 +1,9 @@
(ns re-frame.undo (ns re-frame.undo
(:require-macros [reagent.ratom :refer [reaction]])
(:require (:require
[reagent.core :as reagent]
[re-frame.loggers :refer [console]] [re-frame.loggers :refer [console]]
[re-frame.db :refer [app-db]] [re-frame.db :refer [app-db]]
[re-frame.events :as handlers] [re-frame.events :as handlers]
[re-frame.interop :refer [make-reaction ratom]]
[re-frame.subs :as subs])) [re-frame.subs :as subs]))
@ -32,8 +31,8 @@
;; -- State history ---------------------------------------------------------- ;; -- State history ----------------------------------------------------------
(def ^:private undo-list "A list of history states" (reagent/atom [])) (def ^:private undo-list "A list of history states" (ratom []))
(def ^:private redo-list "A list of future states, caused by undoing" (reagent/atom [])) (def ^:private redo-list "A list of future states, caused by undoing" (ratom []))
;; -- Explanations ----------------------------------------------------------- ;; -- Explanations -----------------------------------------------------------
;; ;;
@ -41,9 +40,9 @@
;; ;;
;; Seems really ugly to have mirrored vectors, but ... ;; Seems really ugly to have mirrored vectors, but ...
;; the code kinda falls out when you do. I'm feeling lazy. ;; the code kinda falls out when you do. I'm feeling lazy.
(def ^:private app-explain "Mirrors app-db" (reagent/atom "")) (def ^:private app-explain "Mirrors app-db" (ratom ""))
(def ^:private undo-explain-list "Mirrors undo-list" (reagent/atom [])) (def ^:private undo-explain-list "Mirrors undo-list" (ratom []))
(def ^:private redo-explain-list "Mirrors redo-list" (reagent/atom [])) (def ^:private redo-explain-list "Mirrors redo-list" (ratom []))
(defn- clear-undos! (defn- clear-undos!
[] []
@ -101,14 +100,14 @@
(fn handler (fn handler
; "returns true if anything is stored in the undo list, otherwise false" ; "returns true if anything is stored in the undo list, otherwise false"
[_ _] [_ _]
(reaction (undos?)))) (make-reaction undos?)))
(subs/register (subs/register
:redos? :redos?
(fn handler (fn handler
; "returns true if anything is stored in the redo list, otherwise false" ; "returns true if anything is stored in the redo list, otherwise false"
[_ _] [_ _]
(reaction (redos?)))) (make-reaction redos?)))
(subs/register (subs/register
@ -116,14 +115,14 @@
(fn handler (fn handler
; "returns a vector of string explanations ordered oldest to most recent" ; "returns a vector of string explanations ordered oldest to most recent"
[_ _] [_ _]
(reaction (undo-explanations)))) (make-reaction undo-explanations)))
(subs/register (subs/register
:redo-explanations :redo-explanations
(fn handler (fn handler
; "returns a vector of string explanations ordered from most recent undo onward" ; "returns a vector of string explanations ordered from most recent undo onward"
[_ _] [_ _]
(reaction (deref redo-explain-list)))) (make-reaction #(deref redo-explain-list))))
;; -- event handlers ---------------------------------------------------------------------------- ;; -- event handlers ----------------------------------------------------------------------------