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."
:url "https://github.com/Day8/re-frame.git"
:license {:name "MIT"}
@ -7,12 +7,13 @@
[reagent "0.6.0-rc"]]
:profiles {:debug {:debug true}
:dev {:dependencies [[karma-reporter "0.3.0"]
[binaryage/devtools "0.7.2"]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-npm "0.6.2"]
[lein-figwheel "0.5.4-7"]
[lein-shell "0.5.0"]]}}
:dev {:dependencies [[karma-reporter "0.3.0"]
[binaryage/devtools "0.7.2"]
[org.clojure/tools.logging "0.3.1"]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-npm "0.6.2"]
[lein-figwheel "0.5.4-7"]
[lein-shell "0.5.0"]]}}
:clean-targets [:target-path "run/compiled"]

View File

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

View File

@ -103,7 +103,7 @@
:stack)]
(try
(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
(throw e)))))))))

View File

@ -1,8 +1,8 @@
(ns re-frame.fx
(:require [reagent.ratom :refer [IReactiveAtom]]
[re-frame.router :refer [dispatch]]
(:require [re-frame.router :refer [dispatch]]
[re-frame.db :refer [app-db]]
[re-frame.events]
[re-frame.interop :refer [ratom? set-timeout!]]
[re-frame.loggers :refer [console]]))
@ -54,7 +54,7 @@
:dispatch-later
(fn [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:
@ -120,7 +120,7 @@
[handler]
(fn fx-handler
[app-db event-vec]
(if-not (satisfies? IReactiveAtom app-db)
(if-not (ratom? 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: \"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
(:require
[reagent.ratom :refer [IReactiveAtom]]
[re-frame.interop :refer [ratom?]]
[re-frame.loggers :refer [console]]
[clojure.data :as data]))
@ -22,7 +22,7 @@
[handler]
(fn pure-handler
[app-db event-vec]
(if-not (satisfies? IReactiveAtom app-db)
(if-not (ratom? app-db)
(do
(if (map? app-db)
(console :warn "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.")

View File

@ -1,7 +1,6 @@
(ns re-frame.router
(:require [reagent.core]
[re-frame.events :refer [handle]]
[goog.async.nextTick]))
(:require [re-frame.events :refer [handle]]
[re-frame.interop :refer [after-render empty-queue next-tick]]))
;; -- Router Loop ------------------------------------------------------------
@ -61,8 +60,8 @@
;; Events can have metadata which says to pause event processing.
;; event metadata -> "run later" functions
(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
:yield goog.async.nextTick}) ;; almost immediately
{:flush-dom (fn [f] (after-render #(next-tick f))) ;; one tick after the end of the next annimation frame
:yield next-tick}) ;; almost immediately
;; Abstract representation of the Event Queue
@ -88,9 +87,9 @@
;; Concrete implementation of IEventQueue
(deftype EventQueue [^:mutable fsm-state
^:mutable queue
^:mutable post-event-callback-fns]
(deftype EventQueue [#?(:cljs ^:mutable fsm-state :clj ^:volatile-mutable fsm-state)
#?(:cljs ^:mutable queue :clj ^:volatile-mutable queue)
#?(:cljs ^:mutable post-event-callback-fns :clj ^:volatile-mutable post-event-callback-fns)]
IEventQueue
;; -- API ------------------------------------------------------------------
@ -144,7 +143,8 @@
[:paused :add-event] [:paused #(-add-event this arg)]
[: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.
(set! fsm-state new-fsm-state)
@ -161,12 +161,12 @@
(handle event-v)
(set! queue (pop queue))
(-call-post-event-callbacks this event-v)
(catch :default ex
(catch #?(:cljs :default :clj Exception) ex
(-fsm-trigger this :exception ex)))))
(-run-next-tick
[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.
;; Be aware that events might have metadata which will pause processing.
@ -182,7 +182,7 @@
(-exception
[_ ex]
(set! queue #queue []) ;; purge the queue
(set! queue empty-queue) ;; purge the queue
(throw ex))
(-pause
@ -206,7 +206,7 @@
;; 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.
;;
(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.
;; 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
stack (->> (js/Error. (str "Event " (first event-v) " dispatched from here:"))
.-stack
clojure.string/split-lines
(remove #(re-find #"react.inc.js|\(native\)" %))
(clojure.string/join "\n"))]
stack #?(:cljs (->> (js/Error. (str "Event " (first event-v) " dispatched from here:"))
.-stack
clojure.string/split-lines
(remove #(re-find #"react\.inc\.js|\(native\)" %))
(clojure.string/join "\n"))
:clj "n/a")]
(if (nil? event-v)
(throw (ex-info "re-frame: you called \"dispatch\" without an event vector." {}))
(push event-queue (with-meta event-v {:stack stack}))))

View File

@ -1,7 +1,7 @@
(ns re-frame.subs
(:require
[reagent.ratom :as ratom :refer [make-reaction] :refer-macros [reaction]]
[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.utils :refer [first-in-vector]]))
@ -38,8 +38,8 @@
[query-v dynv r]
(let [cache-key [query-v dynv]]
;; when this reaction is nolonger being used, remove it from the cache
(ratom/add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
(console :warn "Removing subscription: " cache-key)))
(add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
(console :warn "Removing subscription: " cache-key)))
(console :log "Dispatch site: ")
(console :log (:dispatch-site (meta query-v)))
@ -77,17 +77,17 @@
cached)
(let [query-id (first-in-vector v)
handler-fn (get @qid->fn query-id)]
(when ^boolean js/goog.DEBUG
(when-let [not-reactive (remove #(implements? reagent.ratom/IReactiveAtom %) dynv)]
(when debug-enabled?
(when-let [not-reactive (remove ratom? dynv)]
(console :warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom: " not-reactive)))
(if (nil? handler-fn)
(console :error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")
(let [dyn-vals (reaction (mapv deref dynv))
sub (reaction (handler-fn app-db v @dyn-vals))]
(let [dyn-vals (make-reaction (fn [] (mapv deref dynv)))
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
;; handler-fn returns a reaction which is then wrapped in the sub reaction
;; need to double deref it to get to the actual value.
(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 -------------------
@ -154,22 +154,37 @@
sub-fn ;; first case the user provides a custom sub-fn
(register
sub-name
(fn [db q-vec d-vec]
(let [subscriptions (sub-fn q-vec d-vec)] ;; this let needs to be outside the fn
(ratom/make-reaction
(fn [] (f (multi-deref subscriptions) q-vec d-vec))))))
(fn subs-handler-fn ;; multi-arity to match the arities `subscribe` might invoke.
([db q-vec]
(let [subscriptions (sub-fn q-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
(register
sub-name
(fn [db q-vec d-vec]
(let [subscriptions (map subscribe arrow-subs)
subscriptions (if (< 1 (count subscriptions))
subscriptions
(first subscriptions))] ;; automatically provide a singlton
(ratom/make-reaction
(fn [] (f (multi-deref subscriptions) q-vec d-vec))))))
(letfn [(get-subscriptions []
(let [subscriptions (map subscribe arrow-subs)]
(if (< 1 (count subscriptions))
subscriptions
(first subscriptions))))] ;; automatically provide a singleton
(fn subs-handler-fn
([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
(register ;; the simple case with no subs
sub-name
(fn [db q-vec d-vec]
(ratom/make-reaction (fn [] (f @db q-vec d-vec)))))))())
(fn subs-handler-fn
([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
(:require-macros [reagent.ratom :refer [reaction]])
(:require
[reagent.core :as reagent]
[re-frame.loggers :refer [console]]
[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]))
@ -32,8 +31,8 @@
;; -- State history ----------------------------------------------------------
(def ^:private undo-list "A list of history states" (reagent/atom []))
(def ^:private redo-list "A list of future states, caused by undoing" (reagent/atom []))
(def ^:private undo-list "A list of history states" (ratom []))
(def ^:private redo-list "A list of future states, caused by undoing" (ratom []))
;; -- Explanations -----------------------------------------------------------
;;
@ -41,9 +40,9 @@
;;
;; Seems really ugly to have mirrored vectors, but ...
;; the code kinda falls out when you do. I'm feeling lazy.
(def ^:private app-explain "Mirrors app-db" (reagent/atom ""))
(def ^:private undo-explain-list "Mirrors undo-list" (reagent/atom []))
(def ^:private redo-explain-list "Mirrors redo-list" (reagent/atom []))
(def ^:private app-explain "Mirrors app-db" (ratom ""))
(def ^:private undo-explain-list "Mirrors undo-list" (ratom []))
(def ^:private redo-explain-list "Mirrors redo-list" (ratom []))
(defn- clear-undos!
[]
@ -101,14 +100,14 @@
(fn handler
; "returns true if anything is stored in the undo list, otherwise false"
[_ _]
(reaction (undos?))))
(make-reaction undos?)))
(subs/register
:redos?
(fn handler
; "returns true if anything is stored in the redo list, otherwise false"
[_ _]
(reaction (redos?))))
(make-reaction redos?)))
(subs/register
@ -116,14 +115,14 @@
(fn handler
; "returns a vector of string explanations ordered oldest to most recent"
[_ _]
(reaction (undo-explanations))))
(make-reaction undo-explanations)))
(subs/register
:redo-explanations
(fn handler
; "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 ----------------------------------------------------------------------------