WIP. undo test fails.

This commit is contained in:
Mike Thompson 2016-06-22 10:54:41 +10:00
parent fd50562811
commit 9452998e0b
9 changed files with 359 additions and 186 deletions

View File

@ -26,8 +26,10 @@ Headline:
XXX link to more docs.
- the API for the undo/redo framework has been documented. It existed previously, but it
was not officially there.
- the API for the undo/redo features have been documented and put into `re-frame.core`.
Most of the features existed previously, but now more official.
One feature is new: the ability to undo/redo just part of the `app-db` tree.
https://github.com/Day8/re-frame/wiki/Undo-&-Redo
@ -57,9 +59,12 @@ Breaking:
hook in your own loggers. Otherwise, you have nothing to do.
Improvements
- XXX (full-debug!)
- XXXX middleware for spec checking of event vectors
- XXX todomvc split into simple and advanced.
- XXX What name for reg-pure-sub (too long)
- XXX review todomvc views
- XXX (full-debug!)
- XXX middleware for spec checking of event vectors
- XXX todomvc changed to use spc, instead of Schema
- XXX todomvc split into simple and advanced.
- Bug fix: `post-event-callbacks` were not called when `dispatch-sync` was called.
- added new API `re-frame.core/remove-post-event-callback`. See doc string.

View File

@ -44,33 +44,33 @@ input[type="checkbox"] {
display: none;
}
.todoapp {
#todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
#todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
#todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
#todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
#todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
@ -83,7 +83,7 @@ input[type="checkbox"] {
text-rendering: optimizeLegibility;
}
.new-todo,
#new-todo,
.edit {
position: relative;
margin: 0;
@ -104,14 +104,14 @@ input[type="checkbox"] {
font-smoothing: antialiased;
}
.new-todo {
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
#main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
@ -121,7 +121,7 @@ label[for='toggle-all'] {
display: none;
}
.toggle-all {
#toggle-all {
position: absolute;
top: -55px;
left: -12px;
@ -131,50 +131,50 @@ label[for='toggle-all'] {
border: none; /* Mobile Safari */
}
.toggle-all:before {
#toggle-all:before {
content: '';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
#toggle-all:checked:before {
color: #737373;
}
.todo-list {
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
#todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
#todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
@ -188,17 +188,17 @@ label[for='toggle-all'] {
appearance: none;
}
.todo-list li .toggle:after {
#todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
#todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre;
word-break: break-word;
#todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
@ -206,12 +206,12 @@ label[for='toggle-all'] {
transition: color 0.4s;
}
.todo-list li.completed label {
#todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
@ -226,27 +226,27 @@ label[for='toggle-all'] {
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
#todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
#todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
#todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
#todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
#footer {
color: #777;
padding: 10px 15px;
height: 20px;
@ -254,7 +254,7 @@ label[for='toggle-all'] {
border-top: 1px solid #e6e6e6;
}
.footer:before {
#footer:before {
content: '';
position: absolute;
right: 0;
@ -263,22 +263,22 @@ label[for='toggle-all'] {
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
#todo-count {
float: left;
text-align: left;
}
.todo-count strong {
#todo-count strong {
font-weight: 300;
}
.filters {
#filters {
margin: 0;
padding: 0;
list-style: none;
@ -287,11 +287,11 @@ label[for='toggle-all'] {
left: 0;
}
.filters li {
#filters li {
display: inline;
}
.filters li a {
#filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
@ -300,17 +300,17 @@ label[for='toggle-all'] {
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
#filters li a.selected,
#filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
#filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
#clear-completed,
html #clear-completed:active {
float: right;
position: relative;
line-height: 20px;
@ -319,11 +319,11 @@ html .clear-completed:active {
position: relative;
}
.clear-completed:hover {
#clear-completed:hover {
text-decoration: underline;
}
.info {
#info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
@ -331,17 +331,17 @@ html .clear-completed:active {
text-align: center;
}
.info p {
#info p {
line-height: 1;
}
.info a {
#info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
#info a:hover {
text-decoration: underline;
}
@ -350,16 +350,16 @@ html .clear-completed:active {
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
#toggle-all,
#todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
#todo-list li .toggle {
height: 40px;
}
.toggle-all {
#toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
@ -368,11 +368,12 @@ html .clear-completed:active {
}
@media (max-width: 430px) {
.footer {
#footer {
height: 50px;
}
.filters {
#filters {
bottom: 10px;
}
}

View File

@ -13,8 +13,8 @@
;; -- Debugging aids ----------------------------------------------------------
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
(enable-console-print!)
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
(enable-console-print!) ;; so println writes to console.log
;; -- Routes and History ------------------------------------------------------

View File

@ -1,51 +1,147 @@
(ns todomvc.subs
(:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :refer [register-sub]]))
(:require [re-frame.core :refer [register-pure-sub subscribe]]))
;; register-pure-sub allows us to write subscription handlers without ever
;; using `reaction` directly.
;; This is how you would register a simple handler.
(register-pure-sub
:showing
(fn [db _] ;; db, is the value in app-db
(:showing db))) ;; I repeat: db is a value. Not a ratom. And this fn does not return a reaction, just a value.
;; -- Helpers -----------------------------------------------------------------
;; that `fn` is a pure function
;; Next, the registration of a similar handler is done in two steps.
;; First, we `defn` a pure handler function. Then, we use `register-pure-sub` to register it.
;; Two steps. This is different to the first registration, which was done in one step.
(defn sorted-todos
[db _]
(:todos db))
(register-pure-sub :sorted-todos sorted-todos)
(defn filter-fn-for
[showing-kw]
(case showing-kw
:active (complement :done)
:done :done
:all identity))
;; -------------------------------------------------------------------------------------
;; Beyond Simple Handlers
;;
;; A subscription handler is a function which is re-run when its input signals
;; change. Each time it is rerun, it produces a new output (return value).
;;
;; In the simple case, app-db is the only input signal, as was the case in the two
;; simple subscriptions above. But many subscriptions are not directly dependent on
;; app-db, and instead, depend on a value derived from app-db.
;;
;; Such handlers represent "intermediate nodes" in a signal graph. New values emanate
;; from app-db, and flow out through a signal graph, into and out of these intermediate
;; nodes, before a leaf subscription delivers data into views which render data as hiccup.
;;
;; When writing and registering the handler for an intermediate node, you must nominate
;; one or more input signals (typically one or two).
;;
;; register-pure-sub allows you to supply:
;;
;; 1. a function which returns the input signals. It can return either a single signal or
;; a vector of signals, or a map of where the values are the signals.
;;
;; 2. a function which does the computation. It takes input values and produces a new
;; derived value.
;;
;; In the two simple examples at the top, we only supplied the 2nd of these functions.
;; But now we are dealing with intermediate nodes, we'll need to provide both fns.
;;
(register-pure-sub
:todos
;; This function returns the input signals.
;; In this case, it returns a single signal.
;; Although not required in this example, it is called with two paramters
;; being the two values supplied in the originating `(subscribe X Y)`.
;; X will be the query vector and Y is an advanced feature and out of scope
;; for this explanation.
(fn [query-v _]
(subscribe [:sorted-todos])) ;; returns a single input signal
(defn completed-count
"return the count of todos for which :done is true"
[todos]
(count (filter :done (vals todos))))
;; This 2nd fn does the computation. Data values in, derived data out.
;; It is the same as the two simple subscription handlers up at the top.
;; Except they took the value in app-db as their first argument and, instead,
;; this function takes the value delivered by another input signal, supplied by the
;; function above: (subscribe [:sorted-todos])
;;
;; Subscription handlers can take 3 parameters:
;; - the input signals (a single item, a vector or a map)
;; - the query vector supplied to query-v (the query vector argument
;; to the "subscribe") and the 3rd one is for advanced cases, out of scope for this discussion.
(fn [sorted-todos query-v _]
(vals sorted-todos)))
;; -- Subscription handlers and registration ---------------------------------
(register-sub
:todos ;; usage: (subscribe [:todos])
(fn [db _]
(reaction (vals (:todos @db)))))
(register-sub
;; So here we define the handler for another intermediate node.
;; This time the computation involves two input signals.
;; As a result note:
;; - the first function (which returns the signals, returns a 2-vector)
;; - the second function (which is the computation, destructures this 2-vector as its first parameter)
(register-pure-sub
:visible-todos
(fn [db _]
(reaction (let [filter-fn (filter-fn-for (:showing @db))
todos (vals (:todos @db))]
(filter filter-fn todos)))))
(fn [query-v _] ;; returns a vector of two signals.
[(subscribe [:todos])
(subscribe [:showing])])
(register-sub
(fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values
(let [filter-fn (case showing
:active (complement :done)
:done :done
:all identity)]
(filter filter-fn todos))))
;; -------------------------------------------------------------------------------------
;; HEY, WAIT ON
;;
;; How did those two simple registrations at the top work, I hear you ask?
;; We supplied only one function in those registrations, not two?
;; I'm glad you asked.
;; You see, when the signal-returning-fn is omitted, register-pure-sub provides a default.
;; And it loks like this:
;; (fn [_ _] re-frame.db/app-db)
;; You can see that it returns one signal, and that signal is app-db itself.
;;
;; So that's why those two simple registrations didn't provide a signal-fn, but they
;; still got the value in app-db supplied as that first parameter.
;; -------------------------------------------------------------------------------------
;; SUGAR ?
;; Now for some syntactic sugar...
;; The purpose of the sugar is to remove boilerplate noise. To distill to the essential
;; in 90% of cases.
;; Is this a good idea?
;; If it is a good idea, is this good syntax?
;; Because it is so common to nominate 1 or more input signals,
;; register-pure-sub provides some macro sugar so you can nominate a very minimal
;; vector of input signals. The 1st function is not needed.
;; Here is the example above rewritten using the sugar.
#_(register-pure-sub
:visible-todos
:<- [:todos]
:<- [:showing]
(fn [[todos showing] _]
(let [filter-fn (case showing
:active (complement :done)
:done :done
:all identity)]
(filter filter-fn todos))))
(register-pure-sub
:all-complete?
:<- [:todos]
(fn [[todos] _]
(seq todos)))
(register-pure-sub
:completed-count
(fn [db _]
(reaction (completed-count (:todos @db)))))
(register-sub
:footer-stats
(fn [db _]
(reaction
(let [todos (:todos @db)
completed-count (completed-count todos)
active-count (- (count todos) completed-count)
showing (:showing @db)]
[active-count completed-count showing])))) ;; tuple
:<- [:todos]
(fn [[todos] _]
(count (filter :done todos))))
(register-pure-sub
:footer-counts ;; XXXX different from original. Now does not return showing
:<- [:todos]
:<- [:completed-count]
(fn [[todos completed] _]
[(- (count todos) completed) completed]))

View File

@ -1,19 +1,20 @@
(ns todomvc.views
(:require [reagent.core :as reagent :refer [atom]]
(:require [reagent.core :as reagent]
[re-frame.core :refer [subscribe dispatch]]))
(defn todo-input [{:keys [title on-save on-stop]}]
(let [val (atom title)
(let [val (reagent/atom title)
stop #(do (reset! val "")
(if on-stop (on-stop)))
(when on-stop (on-stop)))
save #(let [v (-> @val str clojure.string/trim)]
(if-not (empty? v) (on-save v))
(when (seq v) (on-save v))
(stop))]
(fn [props]
[:input (merge props
{:type "text"
:value @val
:auto-focus true
:on-blur save
:on-change #(reset! val (-> % .-target .-value))
:on-key-down #(case (.-which %)
@ -21,76 +22,95 @@
27 (stop)
nil)})])))
(def todo-edit (with-meta todo-input
{:component-did-mount #(.focus (reagent/dom-node %))}))
(defn stats-footer
[]
(let [footer-stats (subscribe [:footer-stats])]
(fn []
(let [[active done filter] @footer-stats
props-for (fn [filter-kw txt]
[:a {:class (if (= filter-kw filter) "selected")
:href (str "#/" (name filter-kw))} txt])]
[:footer.footer
[:div
[:span.todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul.filters
[:li (props-for :all "All")]
[:li (props-for :active "Active")]
[:li (props-for :done "Completed")]]
(when (pos? done)
[:button.clear-completed {:on-click #(dispatch [:clear-completed])}
"Clear completed"])]]))))
(defn todo-item
[]
(let [editing (atom false)]
(let [editing (reagent/atom false)]
(fn [{:keys [id done title]}]
[:li {:class (str (if done "completed ")
(if @editing "editing"))}
[:div.view
[:input.toggle {:type "checkbox"
:checked done
:on-change #(dispatch [:toggle-done id])}]
[:label {:on-double-click #(reset! editing true)} title]
[:button.destroy {:on-click #(dispatch [:delete-todo id])}]]
[:div.view
[:input.toggle
{:type "checkbox"
:checked done
:on-change #(dispatch [:toggle-done id])}]
[:label
{:on-double-click #(reset! editing true)}
title]
[:button.destroy
{:on-click #(dispatch [:delete-todo id])}]]
(when @editing
[todo-edit {:class "edit"
:title title
:on-save #(dispatch [:save id %])
:on-stop #(reset! editing false)}])])))
[todo-input
{:class "edit"
:title title
:on-save #(dispatch [:save id %])
:on-stop #(reset! editing false)}])])))
(defn footer
[]
[:footer#info
[:p "Double-click to edit a todo"]])
(defn task-list
[]
(let [visible-todos (subscribe [:visible-todos])
all-complete? (subscribe [:all-complete?])]
(fn []
[:section#main
[:input#toggle-all
{:type "checkbox"
:checked @all-complete?
:on-change #(dispatch [:complete-all-toggle (not @all-complete?)])}]
[:label
{:for "toggle-all"}
"Mark all as complete"]
[:ul#todo-list
(for [todo @visible-todos]
^{:key (:id todo)} [todo-item todo])]])))
(defn footer-controls
[]
(let [footer-stats (subscribe [:footer-counts])
showing (subscribe [:showing])]
(fn []
(let [[active done] @footer-stats
a-fn (fn [filter-kw txt]
[:a {:class (when (= filter-kw @showing) "selected")
:href (str "#/" (name filter-kw))} txt])]
[:footer#footer
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li (a-fn :all "All")]
[:li (a-fn :active "Active")]
[:li (a-fn :done "Completed")]]
(when (pos? done)
[:button#clear-completed {:on-click #(dispatch [:clear-completed])}
"Clear completed"])]))))
(defn task-entry
[]
[:header#header
[:h1 "todos"]
[todo-input
{:id "new-todo"
:placeholder "What needs to be done?"
:on-save #(dispatch [:add-todo %])}]])
(defn todo-list
[visible-todos]
[:ul.todo-list
(for [todo @visible-todos]
^{:key (:id todo)} [todo-item todo])])
(defn todo-app
[]
(let [todos (subscribe [:todos])
visible-todos (subscribe [:visible-todos])
completed-count (subscribe [:completed-count])]
(let [todos (subscribe [:todos])]
(fn []
[:div
[:section.todoapp
[:header#header
[:h1 "todos"]
[todo-input {:class "new-todo"
:placeholder "What needs to be done?"
:on-save #(dispatch [:add-todo %])}]]
(when-not (empty? @todos)
[:div
[:section.main
[:input.toggle-all
{:type "checkbox"
:checked (pos? @completed-count)
:on-change #(dispatch [:complete-all-toggle])}]
[:label {:for "toggle-all"} "Mark all as complete"]
[todo-list visible-todos]]
[stats-footer]])]
[:footer.info
[:p "Double-click to edit a todo"]]])))
[:section#todoapp
[task-entry]
(when (seq @todos)
[task-list])
[footer-controls]]
[footer]])))

View File

@ -7,7 +7,8 @@
[reagent "0.6.0-rc"]]
:profiles {:debug {:debug true}
:dev {:dependencies [[karma-reporter "0.3.0"]]
:dev {:dependencies [[karma-reporter "0.3.0"]
[binaryage/devtools "0.7.0"]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-npm "0.6.1"]
[lein-figwheel "0.5.4-2"]]}}
@ -33,7 +34,7 @@
:compiler {:output-to "run/compiled/test.js"
:source-map "run/compiled/test.js.map"
:output-dir "run/compiled/test"
:optimizations :simple
:optimizations :whitespace
:pretty-print true}}]}
:aliases {"auto" ["do" "clean," "cljsbuild" "auto" "test,"]

View File

@ -11,23 +11,23 @@
;; -- Configuration ----------------------------------------------------------
;;
;;
(def ^:private undo-config (atom {:max-undos 50} ;; Maximum number of undo states maintained
{:path []})) ;; undo and redos will apply to only this path within app-db
(def ^:private config (atom {:max-undos 50 ;; Maximum number of undo states maintained
:path []})) ;; undo and redos will apply to only this path within app-db
(defn set-max-undos!
[n]
(swap! undo-config assoc :max-undos n))
(swap! config assoc :max-undos n))
(defn set-undo-path!
[ks]
(swap! undo-config assoc :path ks))
(swap! config assoc :path ks))
(defn max-undos
[]
(:max-undos @undo-config))
(:max-undos @config))
(defn undo-path-ks
[]
(:path @undo-config))
(:path @config))
;; -- State history ----------------------------------------------------------
@ -132,6 +132,11 @@
[undos cur redos]
(let [u @undos
r (cons @cur @redos)]
(.log js/console "in undo")
(.log js/console cur)
(.log js/console (undo-path-ks))
(.log js/console (last u))
(swap! cur assoc-in (undo-path-ks) (last u))
(reset! redos r)
(reset! undos (pop u))))
@ -148,6 +153,7 @@
:undo ;; usage: (dispatch [:undo n]) n is optional, defaults to 1
(fn handler
[_ [_ n]]
(.log js/console "in :undo handler")
(if-not (undos?)
(warn "re-frame: you did a (dispatch [:undo]), but there is nothing to undo.")
(undo-n (or n 1)))))
@ -210,3 +216,35 @@
(handler db event-vec)))))
(def undoable (with-meta undoable_ {:re-frame-factory-name "undoable"}))
;; middleware
(defn fsm-trigger
[trigger update-fn fsm-path]
(fn fsm-middleware
[handler]
(fn fsm-handler
[db event-v]
(let [new-db (handler db event-v)]
(update-fn new-db event-v trigger fsm-path)))) ;; think about access to event-v
(register-handler
:database-connection
(fsm-tirgger :db-working bootstrap-fsm [:fsms :bnootstrap] )
(fn [db q]
.....)
)
;; state
;; fms-state
;; seen-evetns
;; started-tasks
(defn bootstrap-fsm
[new-db event-v trigger fsm-path]
(let [new-state ]
)
)

View File

@ -2,8 +2,10 @@
(:require [jx.reporter.karma :as karma :include-macros true]
[re-frame.test.middleware]
[re-frame.test.undo]
[re-frame.test.subs]))
[re-frame.test.subs]
[devtools.core :as devtools]))
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
(defn ^:export run [karma]
(karma/run-tests

View File

@ -4,40 +4,50 @@
[re-frame.db :as db]
[re-frame.core :as re-frame]))
(deftest test-undos
;; Create undo history
(undo/set-max-undos! 5)
(undo/clear-history!)
(is (not (undo/undos?)))
(is (not (undo/redos?)))
(doseq [i (range 10)]
(reset! db/app-db i)
(reset! db/app-db {i i})
(undo/store-now! i))
;; Check the undo state is correct
(is (undo/undos?))
(is (not (undo/redos?)))
(is (= [4 5 6 7 8 9] (undo/undo-explanations)))
(is (= [5 6 7 8 9] @undo/undo-list))
(is (= [{5 5} {6 6} {7 7} {8 8} {9 9}] @undo/undo-list))
(.log js/console "----1-----")
;; Undo the actions
(re-frame/dispatch-sync [:undo])
(is (= @db/app-db 9))
(.log js/console "----2-----")
(is (= @db/app-db {9 9}))
(is (undo/redos?))
(re-frame/dispatch-sync [:undo])
(is (= @db/app-db 8))
(is (= @db/app-db {8 8}))
(re-frame/dispatch-sync [:undo])
(is (= @db/app-db 7))
(is (= @db/app-db {7 7}))
(re-frame/dispatch-sync [:undo])
(is (= @db/app-db 6))
(is (= @db/app-db {6 6}))
(re-frame/dispatch-sync [:undo])
(is (= @db/app-db 5))
(is (= @db/app-db {5 5}))
(is (not (undo/undos?)))
(is (undo/redos?))
;; Redo them again
(re-frame/dispatch-sync [:redo 5])
(is (= @db/app-db 9))
(is (= @db/app-db {9 9}))
(is (not (undo/redos?)))
(is (undo/undos?))
(is (= [5 6 7 8 9] @undo/undo-list))
(is (= [{5 5} {6 6} {7 7} {8 8} {9 9}] @undo/undo-list))
;; Clear history
(undo/clear-history!)