mirror of
https://github.com/penpot/penpot.git
synced 2026-03-26 11:11:16 -04:00
446 lines
15 KiB
Clojure
446 lines
15 KiB
Clojure
;; 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))))))))
|