;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns common-tests.undo-stack-test (:require [app.common.data.undo-stack :as sut] [clojure.test :as t])) ;; --- make-stack --- (t/deftest make-stack-creates-empty-stack (let [stack (sut/make-stack)] (t/is (= -1 (:index stack))) (t/is (= [] (:items stack))))) (t/deftest make-stack-returns-nil-on-peek (t/is (nil? (sut/peek (sut/make-stack))))) (t/deftest make-stack-size-is-zero (t/is (= 0 (sut/size (sut/make-stack))))) ;; --- peek --- (t/deftest peek-empty-stack (t/is (nil? (sut/peek (sut/make-stack))))) (t/deftest peek-after-append (let [stack (-> (sut/make-stack) (sut/append :a))] (t/is (= :a (sut/peek stack))))) (t/deftest peek-multiple-items (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c))] (t/is (= :c (sut/peek stack))))) (t/deftest peek-after-undo (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/undo))] (t/is (= :a (sut/peek stack))))) ;; --- append --- (t/deftest append-to-nil-stack (t/is (nil? (sut/append nil :a)))) (t/deftest append-single-item (let [stack (-> (sut/make-stack) (sut/append :a))] (t/is (= 0 (:index stack))) (t/is (= [:a] (:items stack))))) (t/deftest append-multiple-items (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c))] (t/is (= 2 (:index stack))) (t/is (= [:a :b :c] (:items stack))))) (t/deftest append-duplicate-at-current-index-ignored (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :a))] (t/is (= 0 (:index stack))) (t/is (= [:a] (:items stack))))) (t/deftest append-duplicate-after-other-operations (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/undo) (sut/append :a))] ;; appending :a when current is :a should be a no-op (t/is (= 0 (:index stack))) (t/is (= [:a :b] (:items stack))))) (t/deftest append-same-value-at-different-positions-allowed (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :a))] (t/is (= 2 (:index stack))) (t/is (= [:a :b :a] (:items stack))))) (t/deftest append-nil-value-returns-unchanged ;; appending nil when peek is nil returns stack unchanged (let [stack (sut/make-stack) result (sut/append stack nil)] (t/is (= stack result)))) (t/deftest append-complex-values (let [v1 {:id 1 :name "shape"} v2 {:id 2 :name "rect"} stack (-> (sut/make-stack) (sut/append v1) (sut/append v2))] (t/is (= 1 (:index stack))) (t/is (= v2 (sut/peek stack))))) ;; --- append truncates redo history --- (t/deftest append-truncates-redo-history-at-index-greater-than-zero ;; Truncation only happens when index > 0 (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/append :d) (sut/undo) ;; index -> 2, peek :c (sut/append :e))] ;; index > 0, truncates :d (t/is (= 3 (:index stack))) (t/is (= [:a :b :c :e] (:items stack))) (t/is (= :e (sut/peek stack))))) (t/deftest append-at-index-zero-does-not-truncate ;; When index is 0, append just adds to end without truncation (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) ;; index -> 1 (sut/undo) ;; index -> 0 (sut/append :d))] ;; index 0, no truncation: items become [:a :b :c :d] (t/is (= 1 (:index stack))) (t/is (= [:a :b :c :d] (:items stack))) (t/is (= :b (sut/peek stack))))) (t/deftest append-truncates-multiple-redo-items (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/append :d) (sut/append :e) (sut/undo) ;; index -> 3, peek :d (sut/undo) ;; index -> 2, peek :c (sut/append :x))] (t/is (= 3 (:index stack))) (t/is (= [:a :b :c :x] (:items stack))))) ;; --- append respects MAX-UNDO-SIZE --- (t/deftest append-max-undo-size-boundary (let [;; Fill stack to MAX-UNDO-SIZE items stack (reduce (fn [s i] (sut/append s (str "item-" i))) (sut/make-stack) (range sut/MAX-UNDO-SIZE))] (t/is (= (dec sut/MAX-UNDO-SIZE) (:index stack))) (t/is (= sut/MAX-UNDO-SIZE (count (:items stack)))) (t/is (= "item-0" (first (:items stack)))) (t/is (= (str "item-" (dec sut/MAX-UNDO-SIZE)) (sut/peek stack))))) (t/deftest append-exceeds-max-undo-size-removes-oldest (let [;; Fill stack to MAX-UNDO-SIZE + 1 stack (reduce (fn [s i] (sut/append s (str "item-" i))) (sut/make-stack) (range (inc sut/MAX-UNDO-SIZE)))] (t/is (= (dec sut/MAX-UNDO-SIZE) (:index stack))) (t/is (= sut/MAX-UNDO-SIZE (count (:items stack)))) ;; Oldest item was removed (t/is (= "item-1" (first (:items stack)))) (t/is (= (str "item-" sut/MAX-UNDO-SIZE) (sut/peek stack))))) (t/deftest append-far-exceeds-max-undo-size (let [;; Fill stack to MAX-UNDO-SIZE + 10 stack (reduce (fn [s i] (sut/append s (str "item-" i))) (sut/make-stack) (range (+ sut/MAX-UNDO-SIZE 10)))] (t/is (= (dec sut/MAX-UNDO-SIZE) (:index stack))) (t/is (= sut/MAX-UNDO-SIZE (count (:items stack)))) (t/is (= "item-10" (first (:items stack)))))) ;; --- fixup --- (t/deftest fixup-updates-current-item (let [stack (-> (sut/make-stack) (sut/append :a) (sut/fixup :a-updated))] (t/is (= :a-updated (sut/peek stack))) (t/is (= 0 (:index stack))))) (t/deftest fixup-at-middle-of-stack (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) ;; index -> 1 (sut/fixup :b-updated))] (t/is (= :b-updated (sut/peek stack))) (t/is (= [:a :b-updated :c] (:items stack))))) (t/deftest fixup-preserves-index (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/undo) ;; index -> 0 (sut/fixup :a-new))] (t/is (= 0 (:index stack))) (t/is (= :a-new (sut/peek stack))))) ;; --- undo --- (t/deftest undo-single-item (let [stack (-> (sut/make-stack) (sut/append :a) (sut/undo))] (t/is (= 0 (:index stack))) (t/is (= :a (sut/peek stack))))) (t/deftest undo-clamps-to-zero (let [stack (-> (sut/make-stack) (sut/append :a) (sut/undo) (sut/undo))] (t/is (= 0 (:index stack))) (t/is (= :a (sut/peek stack))))) (t/deftest undo-multiple-steps (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) (sut/undo))] (t/is (= 0 (:index stack))) (t/is (= :a (sut/peek stack))))) (t/deftest undo-on-empty-stack ;; undo on empty stack clamps index to 0 (from -1, dec gives -2, max with 0 gives 0) (let [stack (-> (sut/make-stack) (sut/undo))] (t/is (= 0 (:index stack))) (t/is (nil? (sut/peek stack))))) (t/deftest undo-preserves-items (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/undo))] (t/is (= [:a :b] (:items stack))))) ;; --- redo --- (t/deftest redo-after-undo (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/undo) (sut/redo))] (t/is (= 1 (:index stack))) (t/is (= :b (sut/peek stack))))) (t/deftest redo-at-end-of-stack-no-op (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/redo))] (t/is (= 1 (:index stack))) (t/is (= :b (sut/peek stack))))) (t/deftest redo-multiple-steps (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) ;; index -> 1 (sut/undo) ;; index -> 0 (sut/redo) ;; index -> 1 (sut/redo))] ;; index -> 2 (t/is (= 2 (:index stack))) (t/is (= :c (sut/peek stack))))) (t/deftest redo-on-empty-stack (let [stack (-> (sut/make-stack) (sut/redo))] (t/is (= -1 (:index stack))))) (t/deftest redo-not-available-after-truncating-append ;; When index > 0, append truncates redo history (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) ;; index -> 1 (sut/append :x) ;; truncates :c, items [:a :b :x] (sut/redo))] ;; Redo should not work since redo history was truncated (t/is (= 2 (:index stack))) (t/is (= :x (sut/peek stack))))) (t/deftest redo-after-append-at-index-zero ;; When index is 0, append does not truncate, so old items remain (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) ;; index -> 1 (sut/undo) ;; index -> 0 (sut/append :x) ;; index 0, no truncation: [:a :b :c :x] (sut/redo))] ;; redo goes from index 1 to 2, which is :c (t/is (= 2 (:index stack))) (t/is (= :c (sut/peek stack))))) ;; --- size --- (t/deftest size-empty-stack (t/is (= 0 (sut/size (sut/make-stack))))) (t/deftest size-after-appends (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c))] (t/is (= 3 (sut/size stack))))) (t/deftest size-unchanged-after-undo (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo))] ;; size returns (inc index), not count of items (t/is (= 2 (sut/size stack))))) (t/deftest size-is-undo-position (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/undo) (sut/undo))] (t/is (= 1 (sut/size stack))))) ;; --- undo/redo round-trip --- (t/deftest undo-redo-round-trip (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c))] (t/is (= :c (sut/peek stack))) (let [s (sut/undo stack)] (t/is (= :b (sut/peek s))) (let [s (sut/undo s)] (t/is (= :a (sut/peek s))) (let [s (sut/undo s)] ;; At index 0, undo should clamp (t/is (= :a (sut/peek s))) (t/is (= 0 (:index s))) (let [s (sut/redo s)] (t/is (= :b (sut/peek s))) (let [s (sut/redo s)] (t/is (= :c (sut/peek s))) (let [s (sut/redo s)] ;; At end, redo should be no-op (t/is (= :c (sut/peek s))))))))))) ;; --- mixed operations --- (t/deftest append-after-undo-then-redo (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/undo) (sut/undo) (sut/redo) (sut/append :c))] (t/is (= 2 (:index stack))) (t/is (= [:a :b :c] (:items stack))) (t/is (= :c (sut/peek stack))))) (t/deftest fixup-then-append (let [stack (-> (sut/make-stack) (sut/append :a) (sut/fixup :a-fixed) (sut/append :b))] (t/is (= [:a-fixed :b] (:items stack))) (t/is (= :b (sut/peek stack))))) (t/deftest append-identical-different-objects (let [m1 {:x 1} m2 {:x 1} stack (-> (sut/make-stack) (sut/append m1) (sut/append m2))] ;; Maps are equal, so second append should be a no-op (t/is (= 0 (:index stack))) (t/is (= [m1] (:items stack))))) (t/deftest append-maps-not-equal (let [m1 {:x 1} m2 {:x 2} stack (-> (sut/make-stack) (sut/append m1) (sut/append m2))] (t/is (= 1 (:index stack))) (t/is (= [m1 m2] (:items stack))))) (t/deftest sequential-fixup-and-undo (let [stack (-> (sut/make-stack) (sut/append {:id 1 :val "old"}) (sut/append {:id 2 :val "old"}) (sut/fixup {:id 2 :val "new"}) (sut/undo) (sut/fixup {:id 1 :val "updated"}))] (t/is (= {:id 1 :val "updated"} (sut/peek stack))) (t/is (= [{:id 1 :val "updated"} {:id 2 :val "new"}] (:items stack))))) (t/deftest append-undo-append-undo-cycle (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) ;; index 2 (sut/undo) ;; index 1, peek :b (sut/append :d) ;; index 1 > 0, truncates :c, items [:a :b :d] (sut/append :e) ;; items [:a :b :d :e] (sut/undo) ;; index 2, peek :d (sut/undo))] ;; index 1, peek :b (t/is (= 1 (:index stack))) (t/is (= :b (sut/peek stack))) (t/is (= [:a :b :d :e] (:items stack))))) (t/deftest size-grows-then-shrinks-with-undo (let [stack (-> (sut/make-stack) (sut/append :a) (sut/append :b) (sut/append :c) (sut/append :d))] (t/is (= 4 (sut/size stack))) (let [s (sut/undo stack)] (t/is (= 3 (sut/size s))) (let [s (sut/undo s)] (t/is (= 2 (sut/size s))) (let [s (sut/redo s)] (t/is (= 3 (sut/size s))))))))