Make lanes explicit symbol views
This commit is contained in:
parent
fb38990090
commit
e459307a4a
22 changed files with 2183 additions and 2330 deletions
|
|
@ -4,22 +4,22 @@
|
|||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.correction :as correction]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.lane-test :as fixture]))
|
||||
[arthur.domain.sequence-test :as fixture]))
|
||||
|
||||
(defn- channel [doc id path]
|
||||
(get-in doc [:symbols :main :nodes id :channels path]))
|
||||
|
||||
(deftest authors-the-three-motions-as-ordinary-layer-channels
|
||||
(let [doc (fixture/document)
|
||||
constant (correction/add doc :main :girl [:xform :rot]
|
||||
constant (correction/add doc :main :a [:xform :rot]
|
||||
{:id :flat :support [2 5] :motion :constant :delta 1})
|
||||
ramp (correction/add (:clip constant) :main :girl [:xform :rot]
|
||||
ramp (correction/add (:clip constant) :main :a [:xform :rot]
|
||||
{:id :ramp :support [6 9] :motion :ramp :start 0 :end 2})
|
||||
returned (correction/add (:clip ramp) :main :girl [:xform :rot]
|
||||
returned (correction/add (:clip ramp) :main :a [:xform :rot]
|
||||
{:id :return :support [9 12] :motion :return
|
||||
:start 0 :peak 3 :peak-frame 10})
|
||||
c (channel (:clip returned) :girl [:xform :rot])]
|
||||
(is (= :girl (:selection returned)))
|
||||
c (channel (:clip returned) :a [:xform :rot])]
|
||||
(is (= :a (:selection returned)))
|
||||
(is (= [:flat :ramp :return] (mapv :id (:over c))))
|
||||
(is (= [0 0 1 1 1 0 0 1 2 0 3 0]
|
||||
(mapv #(ch/value-at c % nil) (range 12))))
|
||||
|
|
@ -47,19 +47,19 @@
|
|||
put3 (ch/layer :put3 [0 2] :replace (ch/framed [1 2 3]))
|
||||
add3 (ch/layer :add3 [0 2] :offset (ch/framed [1 1 1]))
|
||||
stacked (assoc base :over [put3 add3])
|
||||
doc (assoc-in doc [:symbols :main :nodes :girl :channels [:xform :pos]] stacked)]
|
||||
(is (:refused (correction/remove-layer doc :main :girl [:xform :pos] :put3))
|
||||
doc (assoc-in doc [:symbols :main :nodes :a :channels [:xform :pos]] stacked)]
|
||||
(is (:refused (correction/remove-layer doc :main :a [:xform :pos] :put3))
|
||||
"removing the replacement would expose a wrong-shaped base")
|
||||
(let [conflicted (assoc-in doc [:symbols :main :nodes :girl :channels [:xform :pos] :over 1 :conflict]
|
||||
(let [conflicted (assoc-in doc [:symbols :main :nodes :a :channels [:xform :pos] :over 1 :conflict]
|
||||
"old topology")
|
||||
retried (correction/retry-layer conflicted :main :girl [:xform :pos] :add3)]
|
||||
retried (correction/retry-layer conflicted :main :a [:xform :pos] :add3)]
|
||||
(is (:clip retried))
|
||||
(is (nil? (get-in (:clip retried)
|
||||
[:symbols :main :nodes :girl :channels [:xform :pos] :over 1 :conflict]))))
|
||||
[:symbols :main :nodes :a :channels [:xform :pos] :over 1 :conflict]))))
|
||||
(let [without-replacement (-> doc
|
||||
(assoc-in [:symbols :main :nodes :girl :channels [:xform :pos] :over]
|
||||
(assoc-in [:symbols :main :nodes :a :channels [:xform :pos] :over]
|
||||
[(assoc add3 :conflict "old topology")]))]
|
||||
(is (:refused (correction/retry-layer without-replacement :main :girl
|
||||
(is (:refused (correction/retry-layer without-replacement :main :a
|
||||
[:xform :pos] :add3))
|
||||
"retry refuses when the current effective base still has the wrong shape"))))
|
||||
|
||||
|
|
@ -74,13 +74,13 @@
|
|||
|
||||
(deftest corrections-round-trip-with-identity-support-and-order
|
||||
(let [doc (fixture/document)
|
||||
one (:clip (correction/add doc :main :girl [:xform :rot]
|
||||
one (:clip (correction/add doc :main :a [:xform :rot]
|
||||
{:id :one :support [0 3] :motion :constant :delta 1}))
|
||||
two (:clip (correction/add one :main :girl [:xform :rot]
|
||||
two (:clip (correction/add one :main :a [:xform :rot]
|
||||
{:id :two :support [3 6] :motion :ramp
|
||||
:start 0 :end 2}))
|
||||
back (leaf/clip "u" (leaf/leaves "u" two))]
|
||||
(is (= two back))
|
||||
(is (= [:one :two]
|
||||
(mapv :id (get-in back [:symbols :main :nodes :girl
|
||||
(mapv :id (get-in back [:symbols :main :nodes :a
|
||||
:channels [:xform :rot] :over]))))))
|
||||
|
|
|
|||
|
|
@ -241,11 +241,9 @@
|
|||
made (clip/new-symbol c :outer id 20 u)]
|
||||
(is (= :symbol-1 id))
|
||||
(is (= :symbol-2 (clip/fresh-id made)) "the next one does not collide")
|
||||
(is (= {:id :symbol-1 :name "symbol-1" :fps 30 :frames 180
|
||||
:nodes {:lane clip/lane-node}}
|
||||
(is (= {:id :symbol-1 :name "symbol-1" :fps 30 :frames 180 :nodes {}}
|
||||
(clip/symbol made :symbol-1))
|
||||
"empty but for the lane every symbol is born with, and as long as the
|
||||
rest of what it was placed in")
|
||||
"empty, ordinary, and as long as the rest of what it was placed in")
|
||||
(is (= {:span [0 180] :time {:mode :map :at 20 :rate 1}}
|
||||
(select-keys (get-in made [:symbols :outer :nodes u]) [:span :time])))
|
||||
(is (= #{:symbol-1} (node/sources (get-in made [:symbols :outer :nodes u])))
|
||||
|
|
|
|||
|
|
@ -1,650 +0,0 @@
|
|||
(ns arthur.domain.lane-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.bring :as bring]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pick :as pick]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(defn drawing [id x frames]
|
||||
{:id id :frames frames
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 4)
|
||||
[:xform :pos] (ch/framed [x 0])}}}})
|
||||
|
||||
(defn cel [id source at duration speed]
|
||||
{:id id :kind :instance :parent :girl :z (name id)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document []
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (assoc-in (cel :b :drawing-b 4 4 0)
|
||||
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "cels" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12
|
||||
:nodes {:girl {:id :girl :kind :group :layout :sequence :z "b"
|
||||
:channels {[:xform :pos] (ch/keyed {0 [0 0] 6 [60 0] 12 [0 0]} :linear)}}
|
||||
:a a :b b :insert insert
|
||||
:plate {:id :plate :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 10)
|
||||
[:xform :pos] (ch/keyed {0 [-40 0] 11 [70 0]} :linear)}}}}
|
||||
:drawing-a (drawing :drawing-a 10 1)
|
||||
:drawing-b (drawing :drawing-b 20 1)
|
||||
:wave (assoc-in (drawing :wave 0 10) [:nodes :mark :channels [:xform :pos]]
|
||||
(ch/keyed {0 [0 0] 9 [900 0]} :linear))}}))
|
||||
|
||||
|
||||
(defn sample [doc fs]
|
||||
(let [r (clip/resolver doc :main nil pal/index-of nil)]
|
||||
(into {} (map (fn [f] [f (into {} (map (juxt :node :cx)) (r f))])) fs)))
|
||||
|
||||
(deftest one-lane-mixes-held-drawings-and-playing-content
|
||||
(let [doc (document) at (sample doc (range 12))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= 40 (get-in at [3 [:a :mark]])))
|
||||
(is (= 60 (get-in at [4 [:b :mark]])))
|
||||
(is (= 72 (get-in at [5 [:b :mark]])))
|
||||
(is (= 340 (get-in at [8 [:insert :mark]])))
|
||||
(is (= 610 (get-in at [11 [:insert :mark]])))
|
||||
(is (= (zipmap (range 12) (range -40 80 10))
|
||||
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at)))
|
||||
(is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner")))
|
||||
|
||||
(deftest cel-ripple-keeps-lane-keys-and-moves-cel-corrections
|
||||
(let [doc (document)
|
||||
result (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :a (:selection result)))
|
||||
(is (= 14 (get-in after [:symbols :main :frames])))
|
||||
(is (= [0 6] (node/placed-span (:a nodes))))
|
||||
(is (= [6 10] (node/placed-span (:b nodes))))
|
||||
(is (= [10 14] (node/placed-span (:insert nodes))))
|
||||
(doseq [id [:girl :a :b :insert :plate]]
|
||||
(is (= (get-in doc [:symbols :main :nodes id :channels]) (:channels (nodes id))))
|
||||
(is (= (get-in doc [:symbols :main :nodes id :playback]) (:playback (nodes id)))))
|
||||
(let [at (sample after [5 6 7 10])]
|
||||
(is (= 60 (get-in at [5 [:a :mark]])))
|
||||
(is (= 80 (get-in at [6 [:b :mark]])))
|
||||
(is (= 72 (get-in at [7 [:b :mark]])) "B's correction follows B")
|
||||
(is (= 320 (get-in at [10 [:insert :mark]])) "insert starts on source frame 3"))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= (assoc-in doc [:symbols :main :frames] 14)
|
||||
(:clip (lane/extend-hold after :main :a -2 {})))
|
||||
"shrinking restores content, except the explicitly grown shot")))
|
||||
|
||||
(deftest overflow-and-invalid-edits-are-atomic
|
||||
(let [doc (document)
|
||||
result (lane/extend-hold doc :main :a 2 {})]
|
||||
(is (:refused result))
|
||||
(is (= 14 (:required-frames result)))
|
||||
(is (not (contains? result :clip)))
|
||||
(doseq [delta [0 -4 0.5 js/NaN]]
|
||||
(is (:refused (lane/extend-hold doc :main :a delta {}))))
|
||||
(is (:refused (lane/extend-hold doc :main :insert 1 {})))
|
||||
(is (:refused (lane/extend-hold doc :main :missing 1 {})))))
|
||||
|
||||
(deftest dragging-a-cel-edge-trims-neighbours-or-ripples-them
|
||||
(let [doc (document)
|
||||
plain (:clip (lane/resize-out doc :main :a 6 {}))
|
||||
across (:clip (lane/resize-out doc :main :a 9 {}))
|
||||
ripple (:clip (lane/resize-out doc :main :a 6 {:ripple? true
|
||||
:extent :grow-symbol}))
|
||||
shrink (:clip (lane/resize-out doc :main :a 2 {:ripple? true}))]
|
||||
(is (= [[0 6] [6 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in plain [:symbols :main :nodes]) :girl)))
|
||||
"a normal grow eats the beginning of the adjacent cel")
|
||||
(is (= [[0 9] [9 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in across [:symbols :main :nodes]) :girl)))
|
||||
"a long grow removes wholly consumed cels and trims the survivor")
|
||||
(is (= [[0 6] [6 10] [10 14]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in ripple [:symbols :main :nodes]) :girl)))
|
||||
"shift-grow moves every later cel")
|
||||
(is (= [[0 2] [2 6] [6 10]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in shrink [:symbols :main :nodes]) :girl)))
|
||||
"shift-shrink pulls every later cel left")
|
||||
(is (:refused (lane/resize-out doc :main :a 0 {})))
|
||||
(is (:refused (lane/resize-out doc :main :a 2.5 {})))))
|
||||
|
||||
(deftest the-middle-of-a-cut-rolls-both-edges
|
||||
(let [doc (document)
|
||||
rolled (:clip (lane/roll doc :main :a :b 6))
|
||||
right-only (:clip (lane/resize-in doc :main :b 6))
|
||||
grown-left (:clip (lane/resize-in doc :main :b 2))]
|
||||
(is (= [[0 6] [6 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in rolled [:symbols :main :nodes]) :girl)))
|
||||
"the shared cut moves without moving either clip")
|
||||
(is (= [[0 4] [6 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in right-only [:symbols :main :nodes]) :girl)))
|
||||
"the right side of the junction trims only the right clip")
|
||||
(is (= [[0 2] [2 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in grown-left [:symbols :main :nodes]) :girl)))
|
||||
"growing the right clip left trims the neighbour instead of overlapping")
|
||||
(is (:refused (lane/roll doc :main :a :b 0)))
|
||||
(is (:refused (lane/roll doc :main :a :insert 6)))))
|
||||
|
||||
(deftest a-gap-is-an-uncovered-interval
|
||||
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
|
||||
at (sample doc [3 4 7 8])]
|
||||
(is (= #{:plate} (set (keys (at 4)))))
|
||||
(is (= #{:plate} (set (keys (at 7)))))
|
||||
(is (get-in at [8 [:insert :mark]]))
|
||||
(is (empty? (clip/problems doc)))))
|
||||
|
||||
(deftest validation-rejects-overlap-but-allows-empty-lanes
|
||||
(is (some #(re-find #"overlap" %)
|
||||
(clip/problems (assoc-in (document) [:symbols :main :nodes :b :time :at] 3))))
|
||||
(is (empty? (clip/problems
|
||||
(update-in (document) [:symbols :main :nodes] dissoc :a :b :insert))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :span] [0 ##Inf]))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :playback :speed] -1))))
|
||||
(is (seq (node/problems {:id :old :kind :instance :z "a"
|
||||
:channels {[:source] (ch/framed {:of :wave :in 0})}}))
|
||||
"the obsolete format is rejected"))
|
||||
|
||||
(deftest playback-is-independent-of-property-channel-shape
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :a])
|
||||
keyed (node/toggle-key n [:xform :rot] 0 nil)
|
||||
unkeyed (node/toggle-key keyed [:xform :rot] 0 nil)]
|
||||
(doseq [n [n keyed unkeyed]]
|
||||
(is (= {:symbol :drawing-a :frame 0} (node/placed-frame n 11 1))))
|
||||
(let [n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= {:symbol :wave :frame 5} (node/placed-frame n 2 10)))
|
||||
(is (nil? (node/placed-frame n 7 10)))
|
||||
(is (= 9 (:frame (node/placed-frame (assoc-in n [:playback :end] :hold) 9 10))))
|
||||
(is (= 2 (:frame (node/placed-frame (assoc-in n [:playback :end] :loop) 9 10)))))))
|
||||
|
||||
(deftest navigation-and-hit-testing-use-the-same-source-time
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= 5 (:frame (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= {:at 5 :rate 1} (:time (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
||||
(is (= ((pick/bounds-of doc nil :main n) 2)
|
||||
((pick/bounds-of doc nil :main (assoc-in n [:playback :in] 5)) 0)))))
|
||||
|
||||
(deftest seeking-and-source-reuse-do-not-share-cursors
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
||||
fs [11 0 5 3 8 4 10 1 6 2 9 7]
|
||||
at (sample doc fs)]
|
||||
(is (= at (sample doc (reverse fs))))
|
||||
(is (= at (sample doc (range 12))))
|
||||
(let [edited (assoc-in doc [:symbols :drawing-a :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [99 0]))]
|
||||
(is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]])))
|
||||
(is (= 139 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
|
||||
|
||||
(deftest cel-identities-and-playback-round-trip
|
||||
(let [doc (:clip (lane/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
|
||||
leaves (leaf/leaves :project doc)]
|
||||
(is (= doc (leaf/clip :project leaves)))
|
||||
(is (contains? leaves "clip/project/symbol/main/node/a"))
|
||||
(is (not (contains? leaves "clip/project/symbol/main/channel/girl/source")))
|
||||
(let [{copied :clip ids :ids}
|
||||
(bring/symbols (assoc-in (clip/blank) [:symbols :drawing-a] (drawing :drawing-a 99 1))
|
||||
doc [:main] {})]
|
||||
(is (= :drawing-a-2 (:drawing-a ids)))
|
||||
(is (= #{:drawing-a-2 :drawing-b :wave} (clip/places copied (:main ids))))
|
||||
(is (empty? (clip/problems copied))))))
|
||||
|
||||
(deftest one-transaction-undoes-the-ripple-and-shot-extension
|
||||
(let [doc (document)
|
||||
after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
before-leaves (leaf/leaves :p doc)
|
||||
after-leaves (leaf/leaves :p after)
|
||||
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
|
||||
undo (history/undo h after-leaves)
|
||||
redo (history/redo (:history undo) (:leaves undo))]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= before-leaves (:leaves undo)))
|
||||
(is (= after-leaves (:leaves redo)))))
|
||||
|
||||
(deftest create-lane-and-append-drawings
|
||||
(let [doc (:clip (lane/add-lane (clip/blank) :main :girl))
|
||||
a (:clip (lane/append-drawing doc :main :girl :a :drawing-a {}))
|
||||
b (:clip (lane/append-drawing a :main :girl :b :drawing-b {}))]
|
||||
(is (empty? (clip/problems b)))
|
||||
(is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b]))))
|
||||
(is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed])))
|
||||
(is (:refused (lane/append-drawing b :main :girl :a :new {})))))
|
||||
|
||||
(deftest arbitrary-symbols-drop-into-the-same-lane-and-claim-their-time
|
||||
(let [doc (document)
|
||||
dropped (lane/place-symbol doc nil :main :girl :clip :wave 2
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip dropped)
|
||||
clips (symbol/lane-clips (get-in after [:symbols :main :nodes]) :girl)]
|
||||
(is (= :clip (:selection dropped)))
|
||||
(is (= [[0 2] [2 12]] (mapv node/placed-span clips))
|
||||
"the natural ten-frame symbol claims [2,12), trimming/removing incumbents")
|
||||
(is (= :wave (node/source (second clips))))
|
||||
(is (= 1 (:speed (node/playback-of (second clips))))
|
||||
"a dropped symbol plays; it is not converted into a drawing hold")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest an-existing-symbol-row-can-be-adopted-by-a-lane
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :badge]
|
||||
{:id :badge :kind :instance :z "z"
|
||||
:source {:symbol :wave} :span [0 3]
|
||||
:time {:at 1 :rate 1}
|
||||
:playback {:in 2 :speed 1 :end :stop}})
|
||||
result (lane/adopt doc :main :girl :badge 5
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :badge])]
|
||||
(is (= :girl (:parent n)))
|
||||
(is (= [5 8] (node/placed-span n)))
|
||||
(is (= {:in 2 :speed 1 :end :stop} (:playback n))
|
||||
"adoption changes placement, not source timing")
|
||||
(is (= [[0 4] [4 5] [5 8] [8 12]]
|
||||
(mapv node/placed-span
|
||||
(symbol/lane-clips (get-in after [:symbols :main :nodes]) :girl))))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest fractional-placement-rates-convert-the-hold-delta
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :main :nodes :a :time :rate] 2)
|
||||
(assoc-in [:symbols :main :nodes :a :span] [0 8]))
|
||||
after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
(is (= [0 12] (get-in after [:symbols :main :nodes :a :span])))
|
||||
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b]))))))
|
||||
|
||||
(deftest bare-shapes-agree-in-reference-and-playback
|
||||
(let [sym (drawing :bare 12 1)]
|
||||
(is (= (symbol/eval-frame sym 0 nil pal/index-of nil)
|
||||
((symbol/resolver sym nil pal/index-of nil) 0))
|
||||
"omitted style colour must not crash a missing cursor")))
|
||||
|
||||
(deftest audio-follows-only-the-playing-cel
|
||||
(let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"}
|
||||
:span [0 10]
|
||||
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
|
||||
doc (-> (document)
|
||||
(assoc-in [:symbols :wave :nodes :voice] voice)
|
||||
(assoc-in [:symbols :drawing-a :nodes :voice] voice))
|
||||
[track :as tracks] (nest/audio-tracks doc :main)]
|
||||
(is (= 1 (count tracks)) "the frozen drawing contributes no audio")
|
||||
(is (= [8 12] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track)) "the source in-point trims the audio too")
|
||||
(is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys])))
|
||||
(let [moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
[track] (nest/audio-tracks moved :main)]
|
||||
(is (= [10 14] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track))))
|
||||
(let [fast (-> doc
|
||||
(assoc-in [:symbols :main :nodes :girl :time] {:at 2 :rate 2})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback :speed] 2))
|
||||
[track] (nest/audio-tracks fast :main)]
|
||||
(is (= [6 7.75] (node/placed-span track)))
|
||||
(is (= [3 10] (:span track)))
|
||||
(is (= 4 (get-in track [:time :rate]))))))
|
||||
|
||||
(deftest a-looped-insert-schedules-distinct-audio-intervals
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :wave :frames] 4)
|
||||
(assoc-in [:symbols :wave :nodes :voice]
|
||||
{:id :voice :kind :audio :z "a" :source {:sound "v"} :span [1 3]})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback]
|
||||
{:in 3 :speed 1 :end :loop}))]
|
||||
(is (= [[10 12]] (mapv node/placed-span (nest/audio-tracks doc :main))))))
|
||||
|
||||
(deftest enclosing-retiming-is-respected-when-extending-the-shot
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :girl :time] {:at 8 :rate 2})
|
||||
result (lane/extend-hold doc :main :a 2 {})]
|
||||
(is (= 15 (:required-frames result)))
|
||||
(is (nil? (:clip result)))
|
||||
(is (= 15 (get-in (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))))
|
||||
|
||||
(deftest reuse-shares-content-and-make-unique-decouples-one-cel
|
||||
(let [doc (document)
|
||||
shared (:clip (lane/reuse-drawing doc :main :girl :c :drawing-a
|
||||
{:extent :grow-symbol}))
|
||||
edit (fn [c sym x]
|
||||
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [x 0])))]
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :drawing-a {}))
|
||||
"the shot has to be extended on purpose")
|
||||
(is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (empty? (clip/problems shared)))
|
||||
;; One drawing, two cels: the edit arrives at both.
|
||||
(let [at (sample (edit shared :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 99 (get-in at [12 [:c :mark]]))))
|
||||
(let [unique (:clip (lane/make-unique shared :main :c {}))]
|
||||
(is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c]))))
|
||||
(is (= (:nodes (get-in shared [:symbols :drawing-a]))
|
||||
(:nodes (get-in unique [:symbols :drawing-a-2])))
|
||||
"a copy of the same drawing, not an empty one")
|
||||
(is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a])))
|
||||
"the other cel keeps the original")
|
||||
(let [at (sample (edit unique :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 10 (get-in at [12 [:c :mark]])) "the cel made unique is untouched"))
|
||||
(let [at (sample (edit unique :drawing-a-2 99) [0 12])]
|
||||
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
|
||||
(is (empty? (clip/problems unique))))
|
||||
;; Nothing else places drawing-b, so there is nothing to decouple from.
|
||||
(is (:refused (lane/make-unique doc :main :b {})))
|
||||
(is (:refused (lane/make-unique doc :main :girl {}))
|
||||
"a lane places nothing itself")))
|
||||
|
||||
(deftest duplicate-copies-the-drawing-and-not-the-cel
|
||||
(let [doc (document)
|
||||
made (:clip (lane/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
|
||||
n (get-in made [:symbols :main :nodes :d])]
|
||||
(is (= :drawing-b-2 (node/source n)))
|
||||
(is (= (:nodes (get-in doc [:symbols :drawing-b]))
|
||||
(:nodes (get-in made [:symbols :drawing-b-2]))))
|
||||
(is (= [12 13] (node/placed-span n)))
|
||||
(is (= {:in 0 :speed 0 :end :stop} (:playback n)))
|
||||
(is (nil? (:channels n)) "B's own position correction belongs to B's cel")
|
||||
(is (= (get-in doc [:symbols :main :nodes :b])
|
||||
(get-in made [:symbols :main :nodes :b]))
|
||||
"the drawing duplicated is left as it was")
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest a-shallow-copy-keeps-its-parts-and-a-deep-copy-owns-them
|
||||
;; A drawing assembled from another symbol: copying it shallowly must keep
|
||||
;; using that part, and only an explicit deep copy may promise independence.
|
||||
(let [doc (assoc-in (document) [:symbols :drawing-a :nodes :part]
|
||||
{:id :part :kind :instance :z "b" :span [0 1]
|
||||
:time {:at 0 :rate 1} :source {:symbol :wave}
|
||||
:playback {:in 0 :speed 0 :end :stop}})
|
||||
copy (fn [opts] (:clip (lane/duplicate-drawing
|
||||
doc :main :a :d (merge {:extent :grow-symbol} opts))))
|
||||
shallow (copy {})
|
||||
deep (copy {:deep? true})]
|
||||
(is (= :wave (node/source (get-in shallow [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (nil? (get-in shallow [:symbols :wave-2])))
|
||||
(is (= :wave-2 (node/source (get-in deep [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (= (:nodes (get-in doc [:symbols :wave])) (:nodes (get-in deep [:symbols :wave-2]))))
|
||||
(is (empty? (clip/problems shallow)))
|
||||
(is (empty? (clip/problems deep)))))
|
||||
|
||||
(deftest reuse-refuses-what-would-not-be-a-document
|
||||
(let [doc (document)]
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :nothing-here {})))
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol}))
|
||||
"a symbol cannot go inside itself")
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
|
||||
"a cel ID in use is not free")
|
||||
(is (:refused (lane/reuse-drawing doc :main :plate :c :drawing-a {})))
|
||||
(is (:refused (lane/duplicate-drawing doc :main :girl :d {})))))
|
||||
|
||||
(deftest drawing-on-twos-does-not-quantize-the-lane-transform
|
||||
;; Cel length IS the drawing cadence, and it is the only thing on twos
|
||||
;; here: the lane's transform has its own clock and keeps moving every frame.
|
||||
;; Stepping it would be the cel cadence leaking into continuous motion.
|
||||
(let [cel (fn [id source at] (cel id source at 2 0))
|
||||
doc (-> (document)
|
||||
(update-in [:symbols :main :nodes] dissoc :a :b :insert)
|
||||
(update-in [:symbols :main :nodes] merge
|
||||
{:c0 (cel :c0 :drawing-a 0)
|
||||
:c1 (cel :c1 :drawing-b 2)
|
||||
:c2 (cel :c2 :drawing-a 4)}))
|
||||
xs {:c0 10 :c1 20 :c2 10}
|
||||
at (sample doc (range 6))
|
||||
showing (fn [f] (first (dissoc (at f) :plate)))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= [:c0 :c0 :c1 :c1 :c2 :c2] (mapv #(first (key (showing %))) (range 6)))
|
||||
"the drawing showing changes every second frame")
|
||||
(is (= [0 10 20 30 40 50]
|
||||
(mapv (fn [f] (let [[[id _] cx] (showing f)] (- cx (xs id)))) (range 6)))
|
||||
"and the lane moves on every frame, odd ones included")))
|
||||
|
||||
(defn- drawn
|
||||
"What every frame draws, as sorted values, so a picture can be compared
|
||||
without naming the cels that produced it."
|
||||
[doc fs]
|
||||
(let [at (sample doc fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
||||
(deftest a-drawing-goes-anywhere-in-the-lane-and-ripples-what-follows
|
||||
(let [doc (document)
|
||||
keys-of #(get-in % [:symbols :main :nodes :girl :channels [:xform :pos] :keys])
|
||||
spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id])))
|
||||
[:a :n :b :insert])
|
||||
r (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 4 :extent :grow-symbol})]
|
||||
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
|
||||
(is (= 13 (get-in r [:clip :symbols :main :frames])))
|
||||
(is (= (keys-of doc) (keys-of (:clip r))) "lane keys stay where they were authored")
|
||||
(is (= :n (:selection r)))
|
||||
(is (= 4 (:frame r)))
|
||||
(is (empty? (clip/problems (:clip r))))
|
||||
;; The same command with no room refuses, and says how much it needs.
|
||||
(is (= 13 (:required-frames (lane/append-drawing doc :main :girl :n :drawing-n {:at 4}))))
|
||||
;; At the very front everything moves.
|
||||
(is (= [[1 5] [0 1] [5 9] [9 13]]
|
||||
(spans (:clip (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 0 :extent :grow-symbol})))))
|
||||
;; Inside a cel is not a position for another one.
|
||||
(is (re-find #"split it first"
|
||||
(:refused (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol}))))
|
||||
(is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at -1 :extent :grow-symbol})))
|
||||
(is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at ##Inf :extent :grow-symbol})))
|
||||
;; Reuse and duplicate take a position too; it is one placement rule.
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (lane/reuse-drawing doc :main :girl :n :drawing-b
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (lane/duplicate-drawing doc :main :b :n
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))))
|
||||
|
||||
(deftest split-then-place-puts-a-drawing-inside-a-hold
|
||||
;; The two commands the doc asks for, composed: neither one guesses.
|
||||
(let [doc (document)
|
||||
cut (:clip (span/split doc :main :a 2 :right))
|
||||
r (lane/append-drawing cut :main :girl :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol})
|
||||
after (:clip r)]
|
||||
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
|
||||
(mapv #(node/placed-span (get-in after [:symbols :main :nodes %]))
|
||||
[:a :n :right :b :insert])))
|
||||
(is (= (get-in doc [:symbols :main :nodes :girl :channels])
|
||||
(get-in after [:symbols :main :nodes :girl :channels]))
|
||||
"the performance is still timed the way it was authored")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-three-frame-correction-crosses-a-drawing-boundary
|
||||
;; The lane model's worked example. The correction belongs to the GIRL, so it
|
||||
;; applies across whichever drawings are showing under it, and outside its
|
||||
;; three frames the animation evaluates exactly as it did before.
|
||||
(let [doc (document)
|
||||
fs (range 12)
|
||||
before (drawn doc fs)
|
||||
beat (ch/layer :beat [3 6] :offset (ch/framed [30 0]))
|
||||
c (update-in doc [:symbols :main :nodes :girl :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
after (drawn c fs)
|
||||
outside [0 1 2 6 7 8 9 10 11]]
|
||||
(is (empty? (clip/problems c)))
|
||||
(is (= (mapv before outside) (mapv after outside))
|
||||
"outside the support, frame for frame identical")
|
||||
(let [at (sample c [3 4 5])]
|
||||
;; Frame 3 shows drawing A and frames 4 and 5 show drawing B: one
|
||||
;; correction, reaching across the cut between them.
|
||||
(is (= 70 (get-in at [3 [:a :mark]])))
|
||||
(is (= 90 (get-in at [4 [:b :mark]])))
|
||||
(is (= 102 (get-in at [5 [:b :mark]])) "and B's own correction still applies under it")
|
||||
(is (= [-10 0 10] (mapv (fn [f] (js/Math.round (get-in at [f :plate]))) [3 4 5]))
|
||||
"while the background, which is not in the lane, does not move"))
|
||||
;; One document change: one step, and it persists in the channel's own leaf.
|
||||
(let [b (leaf/leaves :p doc)
|
||||
a (leaf/leaves :p c)
|
||||
h (-> nil history/hold (history/record b a 0) history/settle)]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= b (:leaves (history/undo h a))))
|
||||
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
|
||||
|
||||
(deftest a-correction-on-one-cel-travels-with-it
|
||||
;; The other half of ownership: a layer on a cel is in that
|
||||
;; cel's own frames, so moving the cel moves the correction and
|
||||
;; nothing has to say so.
|
||||
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
|
||||
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
;; Stated as the difference from the same document without the correction,
|
||||
;; so the claim is about WHERE the layer applies and not about arithmetic.
|
||||
(let [nudge (fn [with without f]
|
||||
(- (get-in (sample with [f]) [f [:b :mark]])
|
||||
(get-in (sample without [f]) [f [:b :mark]])))]
|
||||
(is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7]))
|
||||
"B's first two frames, which are lane frames 4 and 5")
|
||||
(is (= [7 7 0 0]
|
||||
(mapv #(nudge moved (:clip (lane/extend-hold (document) :main :a 2
|
||||
{:extent :grow-symbol}))
|
||||
%)
|
||||
[6 7 8 9]))
|
||||
"and after A's hold grows, B's first two frames, which are now 6 and 7"))
|
||||
(is (= (get-in doc [:symbols :main :nodes :b :channels])
|
||||
(get-in moved [:symbols :main :nodes :b :channels]))
|
||||
"the layer itself was not touched by the retiming")
|
||||
(is (empty? (clip/problems moved)))))
|
||||
|
||||
(defn- spans [clip ids]
|
||||
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
|
||||
|
||||
(deftest blanking-leaves-a-gap-and-does-not-close-it
|
||||
(let [doc (document)
|
||||
r (lane/blank doc :main :girl [5 7] {:id :rest})
|
||||
after (:clip r)]
|
||||
;; B spanned the range, so it became two cels with a hole between them.
|
||||
(is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert])))
|
||||
(is (= :rest (:selection r)))
|
||||
(let [at (sample after [4 5 6 7])]
|
||||
(is (= #{:plate} (set (keys (at 5)))) "nothing is drawn on a blanked frame")
|
||||
(is (= #{:plate} (set (keys (at 6)))))
|
||||
(is (get-in at [4 [:b :mark]]))
|
||||
(is (get-in at [7 [:rest :mark]])))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-whole-cel-removes-it-and-keeps-its-drawing
|
||||
(let [doc (document)
|
||||
after (:clip (lane/blank doc :main :girl [4 8] {}))]
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
|
||||
(is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b]))
|
||||
"a lane does not own its content")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-range-trims-what-it-only-partly-covers
|
||||
(let [doc (document)
|
||||
after (:clip (lane/blank doc :main :girl [3 9] {}))]
|
||||
(is (= [[0 3] [9 12]] (spans after [:a :insert])))
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= (get-in (sample doc [9]) [9 [:insert :mark]])
|
||||
(get-in (sample after [9]) [9 [:insert :mark]]))
|
||||
"the insert kept its own frames, so frame 9 shows what it showed")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest overwrite-clears-one-frame-and-does-not-ripple-what-follows
|
||||
(let [r (lane/overwrite-drawing (document) :main :girl :n :drawing-n 5
|
||||
{:extent :keep :remainder-id :right})
|
||||
after (:clip r)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :n (:selection r)))
|
||||
(is (= [[0 4] [4 5] [5 6] [6 8] [8 12]]
|
||||
(mapv #(node/placed-span (get nodes %)) [:a :b :n :right :insert])))
|
||||
(is (= :drawing-b (node/source (:right nodes))))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
||||
(let [doc (document)]
|
||||
(is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
|
||||
"splitting a cel needs an ID for the remainder")
|
||||
(is (:refused (lane/blank doc :main :girl [5 7] {:id :a})) "and a free one")
|
||||
(is (:refused (lane/blank doc :main :girl [7 5] {})))
|
||||
(is (:refused (lane/blank doc :main :girl [5 5] {})))
|
||||
(is (:refused (lane/blank doc :main :girl [5 6.5] {})))
|
||||
(is (:refused (lane/blank doc :main :plate [0 2] {})))))
|
||||
|
||||
(deftest the-shot-length-is-authored-and-emptying-a-lane-does-not-shorten-it
|
||||
;; The window and the occupied extent are two facts. A shot with nothing in
|
||||
;; the last half is a shot somebody authored that long, and deleting the last
|
||||
;; drawing must not quietly shorten the film.
|
||||
(let [doc (document)
|
||||
empty-lane (:clip (lane/blank doc :main :girl [0 12] {}))]
|
||||
(is (empty? (symbol/lane-clips (get-in empty-lane [:symbols :main :nodes]) :girl)))
|
||||
(is (= 12 (get-in empty-lane [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems empty-lane)))
|
||||
;; Growing is still the caller's word, and only ever grows.
|
||||
(is (:refused (lane/append-drawing empty-lane :main :girl :n :drawing-n {:at 20})))
|
||||
(is (= 21 (get-in (lane/append-drawing empty-lane :main :girl :n :drawing-n
|
||||
{:at 20 :extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))
|
||||
(is (= 12 (get-in (:clip (span/trim doc :main :insert :out 9))
|
||||
[:symbols :main :frames]))
|
||||
"and trimming the last cel leaves the window where it was")))
|
||||
|
||||
(deftest a-take-placed-in-a-lane-is-still-heard
|
||||
;; `bring/take` puts a take's sound INSIDE the symbol it makes, so that
|
||||
;; "wherever the symbol is placed it is heard". A lane is one of the places it
|
||||
;; can be placed, and must not be the one place that goes silent.
|
||||
(let [doc (assoc-in (document) [:symbols :take]
|
||||
{:id :take :frames 10 :fps 24
|
||||
:nodes {:pic {:id :pic :kind :instance :z "a"
|
||||
:source {:symbol :wave} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:playback {:in 0 :speed 1 :end :stop}}
|
||||
:sound {:id :sound :name "sound" :kind :audio
|
||||
:parent nil :z "z-sound"
|
||||
:source {:footage "f1"} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}}}})
|
||||
at-root (clip/place-symbol doc nil :main :take 0 :root nil)
|
||||
in-lane (:clip (lane/place-symbol doc nil :main :girl :drop :take 0
|
||||
{:extent :grow-symbol :remainder-id :tail}))]
|
||||
(is (= 1 (count (nest/audio-tracks at-root :main)))
|
||||
"a take placed at the root is heard")
|
||||
(is (some? in-lane) "the take goes into the lane")
|
||||
(is (= 1 (count (nest/audio-tracks in-lane :main)))
|
||||
"and is still heard from inside a lane")))
|
||||
|
||||
(deftest a-sound-is-a-clip-in-a-lane-like-any-other
|
||||
;; Everything in the timeline is a lane, audio included: a sound claims lane
|
||||
;; time by the same rule, and what a lane will not do is hold both kinds.
|
||||
(let [made (lane/add-lane (document) :main :track)
|
||||
seeded (clip/place-sound (:clip made) :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
result (lane/adopt seeded :main :track :vo 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :vo])]
|
||||
(is (nil? (:refused result)) (str (:refused result)))
|
||||
(is (= :track (:parent n)))
|
||||
(is (= [2 8] (node/placed-span n)))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= 1 (count (nest/audio-tracks after :main)))
|
||||
"a sound in a lane is still heard")
|
||||
(is (= [:vo] (mapv :id (symbol/lane-clips (get-in after [:symbols :main :nodes]) :track))))
|
||||
;; The one thing a lane refuses: being half picture and half sound, which
|
||||
;; is the explicit capability rather than a guess per frame.
|
||||
(let [mixed (lane/place-symbol after nil :main :track :also :wave 2
|
||||
{:extent :grow-symbol :remainder-id :rest})]
|
||||
(is (:refused mixed))
|
||||
(is (re-find #"picture or sound" (str (:refused mixed))))
|
||||
(is (= [:vo] (mapv :id (symbol/lane-clips (get-in after [:symbols :main :nodes]) :track)))
|
||||
"and the sound it would have had to delete to make room is still there"))))
|
||||
812
frontend/test/arthur/domain/sequence_test.cljs
Normal file
812
frontend/test/arthur/domain/sequence_test.cljs
Normal file
|
|
@ -0,0 +1,812 @@
|
|||
(ns arthur.domain.sequence-test
|
||||
"The commands that need a SEQUENCE, which is now a symbol drawn as a lane and
|
||||
its own children rather than a group with `:layout :sequence`. Was
|
||||
`lane_test`; the fixtures changed and the assertions did not, except where
|
||||
they are called out below as behaviour that changed on purpose."
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.bring :as bring]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pick :as pick]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(defn drawing [id x frames]
|
||||
{:id id :frames frames
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 4)
|
||||
[:xform :pos] (ch/framed [x 0])}}}})
|
||||
|
||||
(defn cel
|
||||
"A clip of the sequence. PARENTLESS, because the symbol is the container."
|
||||
[id source at duration speed]
|
||||
{:id id :kind :instance :z (name id)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document
|
||||
"`:main`, drawn as a lane, holding three clips and a background shape.
|
||||
|
||||
`:plate` HAS NO SPAN, so it is on screen for the whole shot and is not in the
|
||||
sequence at all — which is what `symbol/children` skips, and the reason an
|
||||
ordinary shape sitting in a lane symbol is not something an edge edit can trim."
|
||||
[]
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (assoc-in (cel :b :drawing-b 4 4 0)
|
||||
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "cels" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12 :display :lane
|
||||
:nodes {:a a :b b :insert insert
|
||||
:plate {:id :plate :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 10)
|
||||
[:xform :pos] (ch/keyed {0 [-40 0] 11 [70 0]} :linear)}}}}
|
||||
:drawing-a (drawing :drawing-a 10 1)
|
||||
:drawing-b (drawing :drawing-b 20 1)
|
||||
:wave (assoc-in (drawing :wave 0 10) [:nodes :mark :channels [:xform :pos]]
|
||||
(ch/keyed {0 [0 0] 9 [900 0]} :linear))}}))
|
||||
|
||||
(defn shot
|
||||
"`doc` with `:main` placed in a containing symbol as the instance `:girl`,
|
||||
carrying the keyed transform the LANE GROUP used to carry, and the plate moved
|
||||
out to be the background that is not in the sequence.
|
||||
|
||||
THE PLACING INSTANCE IS WHERE THE LANE'S TRANSFORM WENT. A lane was a group
|
||||
and could be animated; a symbol cannot, so what moves a whole sequence is now
|
||||
the instance that places it. These are the tests that used to animate `:girl`
|
||||
the group and now animate `:girl` the instance — the same six keys, and the
|
||||
same answer frame for frame."
|
||||
[doc]
|
||||
(-> doc
|
||||
(update-in [:symbols :main :nodes] dissoc :plate)
|
||||
(assoc-in [:symbols :shot]
|
||||
{:id :shot :frames 12 :fps 24
|
||||
:nodes {:girl {:id :girl :kind :instance :z "b" :span [0 12]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:source {:symbol :main}
|
||||
:playback {:in 0 :speed 1 :end :stop}
|
||||
:channels {[:xform :pos]
|
||||
(ch/keyed {0 [0 0] 6 [60 0] 12 [0 0]} :linear)}}
|
||||
:plate (get-in doc [:symbols :main :nodes :plate])}})))
|
||||
|
||||
(defn sample-in [doc sid fs]
|
||||
(let [r (clip/resolver doc sid nil pal/index-of nil)]
|
||||
(into {} (map (fn [f] [f (into {} (map (juxt :node :cx)) (r f))])) fs)))
|
||||
|
||||
(defn sample [doc fs] (sample-in doc :main fs))
|
||||
|
||||
(deftest one-sequence-mixes-held-drawings-and-playing-content
|
||||
(let [doc (document) at (sample doc (range 12))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= 10 (get-in at [3 [:a :mark]])))
|
||||
(is (= 20 (get-in at [4 [:b :mark]])))
|
||||
(is (= 22 (get-in at [5 [:b :mark]])))
|
||||
(is (= 300 (get-in at [8 [:insert :mark]])))
|
||||
(is (= 600 (get-in at [11 [:insert :mark]])))
|
||||
(is (= (zipmap (range 12) (range -40 80 10))
|
||||
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at)))
|
||||
(is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner")))
|
||||
|
||||
(deftest the-placing-instance-animates-the-whole-sequence
|
||||
;; What `:girl` the lane group used to do, `:girl` the instance does: one
|
||||
;; transform over whichever drawing is showing under it, every frame.
|
||||
(let [doc (shot (document)) at (sample-in doc :shot (range 12))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= 40 (get-in at [3 [:girl :a :mark]])))
|
||||
(is (= 60 (get-in at [4 [:girl :b :mark]])))
|
||||
(is (= 72 (get-in at [5 [:girl :b :mark]])))
|
||||
(is (= 340 (get-in at [8 [:girl :insert :mark]])))
|
||||
(is (= 610 (get-in at [11 [:girl :insert :mark]])))
|
||||
(is (= (zipmap (range 12) (range -40 80 10))
|
||||
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at))
|
||||
"and the background, which is not in the sequence, does not move with it")))
|
||||
|
||||
(deftest cel-ripple-keeps-the-containers-keys-and-moves-cel-corrections
|
||||
(let [doc (shot (document))
|
||||
result (span/extend-hold doc :main :a 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :a (:selection result)))
|
||||
(is (= 14 (get-in after [:symbols :main :frames])))
|
||||
(is (= [0 6] (node/placed-span (:a nodes))))
|
||||
(is (= [6 10] (node/placed-span (:b nodes))))
|
||||
(is (= [10 14] (node/placed-span (:insert nodes))))
|
||||
(doseq [id [:a :b :insert]]
|
||||
(is (= (get-in doc [:symbols :main :nodes id :channels]) (:channels (nodes id))))
|
||||
(is (= (get-in doc [:symbols :main :nodes id :playback]) (:playback (nodes id)))))
|
||||
(is (= (get-in doc [:symbols :shot :nodes :girl])
|
||||
(get-in after [:symbols :shot :nodes :girl]))
|
||||
"re-spanning a sequence does not touch what places it")
|
||||
(let [at (sample-in after :shot [5 6 7 10])]
|
||||
(is (= 60 (get-in at [5 [:girl :a :mark]])))
|
||||
(is (= 80 (get-in at [6 [:girl :b :mark]])))
|
||||
(is (= 72 (get-in at [7 [:girl :b :mark]])) "B's correction follows B")
|
||||
(is (= 320 (get-in at [10 [:girl :insert :mark]])) "insert starts on source frame 3"))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= (assoc-in doc [:symbols :main :frames] 14)
|
||||
(:clip (span/extend-hold after :main :a -2 {})))
|
||||
"shrinking restores content, except the explicitly grown shot")))
|
||||
|
||||
(deftest overflow-and-invalid-edits-are-atomic
|
||||
(let [doc (document)
|
||||
result (span/extend-hold doc :main :a 2 {})]
|
||||
(is (:refused result))
|
||||
(is (= 14 (:required-frames result)))
|
||||
(is (not (contains? result :clip)))
|
||||
(doseq [delta [0 -4 0.5 js/NaN]]
|
||||
(is (:refused (span/extend-hold doc :main :a delta {}))))
|
||||
(is (:refused (span/extend-hold doc :main :insert 1 {})))
|
||||
(is (:refused (span/extend-hold doc :main :missing 1 {})))
|
||||
(is (:refused (span/extend-hold (update-in doc [:symbols :main] dissoc :display)
|
||||
:main :a 2 {:extent :grow-symbol}))
|
||||
"lengthening a hold is a sequence edit, so it needs a symbol drawn as one")))
|
||||
|
||||
(deftest dragging-a-cel-edge-trims-neighbours-or-ripples-them
|
||||
(let [doc (document)
|
||||
clips #(mapv node/placed-span (symbol/children (get-in % [:symbols :main :nodes])))
|
||||
plain (:clip (span/resize-out doc :main :a 6 {}))
|
||||
across (:clip (span/resize-out doc :main :a 9 {}))
|
||||
ripple (:clip (span/resize-out doc :main :a 6 {:ripple? true
|
||||
:extent :grow-symbol}))
|
||||
shrink (:clip (span/resize-out doc :main :a 2 {:ripple? true}))]
|
||||
(is (= [[0 6] [6 8] [8 12]] (clips plain))
|
||||
"a normal grow eats the beginning of the adjacent cel")
|
||||
(is (= [[0 9] [9 12]] (clips across))
|
||||
"a long grow removes wholly consumed cels and trims the survivor")
|
||||
(is (= [[0 6] [6 10] [10 14]] (clips ripple))
|
||||
"shift-grow moves every later cel")
|
||||
(is (= [[0 2] [2 6] [6 10]] (clips shrink))
|
||||
"shift-shrink pulls every later cel left")
|
||||
(is (:refused (span/resize-out doc :main :a 0 {})))
|
||||
(is (:refused (span/resize-out doc :main :a 2.5 {})))))
|
||||
|
||||
(deftest outside-lane-mode-an-edge-edit-disturbs-nothing
|
||||
;; The claim-time rule FOLLOWS THE MODE. The same document not drawn as a lane
|
||||
;; is a composition, and things in a composition are allowed to be on screen
|
||||
;; together — so growing one clip over another simply does that.
|
||||
(let [doc (update-in (document) [:symbols :main] dissoc :display)
|
||||
after (:clip (span/resize-out doc :main :a 6 {}))
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= [0 6] (node/placed-span (:a nodes))))
|
||||
(is (= [4 8] (node/placed-span (:b nodes))) "the neighbour is left exactly as it was")
|
||||
(is (= 1 (count (symbol/overlaps (get-in after [:symbols :main])))))
|
||||
(is (empty? (clip/problems after)) "and an overlap is not a reason a document will not load")))
|
||||
|
||||
(deftest the-middle-of-a-cut-rolls-both-edges
|
||||
(let [doc (document)
|
||||
clips #(mapv node/placed-span (symbol/children (get-in % [:symbols :main :nodes])))
|
||||
rolled (:clip (span/roll doc :main :a :b 6))
|
||||
right-only (:clip (span/resize-in doc :main :b 6))
|
||||
grown-left (:clip (span/resize-in doc :main :b 2))]
|
||||
(is (= [[0 6] [6 8] [8 12]] (clips rolled))
|
||||
"the shared cut moves without moving either clip")
|
||||
(is (= [[0 4] [6 8] [8 12]] (clips right-only))
|
||||
"the right side of the junction trims only the right clip")
|
||||
(is (= [[0 2] [2 8] [8 12]] (clips grown-left))
|
||||
"growing the right clip left trims the neighbour instead of overlapping")
|
||||
(is (:refused (span/roll doc :main :a :b 0)))
|
||||
(is (:refused (span/roll doc :main :a :insert 6)))
|
||||
(is (:refused (span/roll (update-in doc [:symbols :main] dissoc :display) :main :a :b 6))
|
||||
"a roll is a sequence edit")))
|
||||
|
||||
(deftest a-gap-is-an-uncovered-interval
|
||||
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
|
||||
at (sample doc [3 4 7 8])]
|
||||
(is (= #{:plate} (set (keys (at 4)))))
|
||||
(is (= #{:plate} (set (keys (at 7)))))
|
||||
(is (get-in at [8 [:insert :mark]]))
|
||||
(is (empty? (clip/problems doc)))))
|
||||
|
||||
(deftest an-overlap-is-a-bug-report-and-not-a-reason-not-to-load
|
||||
;; It used to be `clip/problems`, which means the document will not load. A
|
||||
;; display hint must never be able to do that, so it is its own diagnostic.
|
||||
(let [clashing (assoc-in (document) [:symbols :main :nodes :b :time :at] 3)]
|
||||
(is (= [[:a :b]] (symbol/overlaps (get-in clashing [:symbols :main]))))
|
||||
(is (empty? (clip/problems clashing))
|
||||
"and the document still loads, to be drawn visibly wrong")
|
||||
(is (empty? (symbol/overlaps (get-in (document) [:symbols :main]))))
|
||||
(is (empty? (clip/problems
|
||||
(update-in (document) [:symbols :main :nodes] dissoc :a :b :insert)))
|
||||
"an empty sequence is valid — a shot is authored before it is filled")
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :span] [0 ##Inf]))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :playback :speed] -1))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :layout] :sequence)))
|
||||
":layout is not a node field any more")
|
||||
(is (seq (node/problems {:id :old :kind :instance :z "a"
|
||||
:channels {[:source] (ch/framed {:of :wave :in 0})}}))
|
||||
"the obsolete format is rejected")))
|
||||
|
||||
(deftest no-command-can-commit-an-overlap
|
||||
;; THE INVARIANT, ASSERTED OVER THE COMMANDS rather than reasoned about: every
|
||||
;; one of them commits through `span/finish`, so sampling them is enough.
|
||||
;; Sampled, in the style of `drawn`, rather than computing expected spans by
|
||||
;; hand — what is being claimed is that no result holds an overlap, whatever
|
||||
;; the numbers are.
|
||||
(let [doc (document)
|
||||
every-command
|
||||
(concat
|
||||
(for [to (range -1 15)] #(span/resize-out % :main :a to {:extent :grow-symbol}))
|
||||
(for [to (range -1 15)] #(span/resize-out % :main :b to {:ripple? true :extent :grow-symbol}))
|
||||
(for [to (range -1 15)] #(span/resize-in % :main :b to))
|
||||
(for [to (range -1 15)] #(span/roll % :main :a :b to))
|
||||
(for [cut (range -1 15)] #(span/split % :main :insert cut :piece))
|
||||
(for [to (range -1 15)] #(span/trim % :main :insert :out to))
|
||||
(for [to (range -1 15)] #(span/move % :main :insert to))
|
||||
(for [d (range -5 6)] #(span/extend-hold % :main :a d {:extent :grow-symbol}))
|
||||
(for [a (range 0 13) b (range 0 13)] #(span/blank % :main [a b] {:id :rest}))
|
||||
(for [at (range -1 15)] #(span/append-drawing % :main :n :drawing-n
|
||||
{:at at :extent :grow-symbol}))
|
||||
(for [at (range -1 15)] #(span/reuse-drawing % :main :n :drawing-b
|
||||
{:at at :extent :grow-symbol}))
|
||||
(for [at (range -1 15)] #(span/duplicate-drawing % :main :b :n
|
||||
{:at at :extent :grow-symbol}))
|
||||
(for [at (range -1 15)] #(span/overwrite-drawing % :main :n :drawing-n at
|
||||
{:extent :grow-symbol
|
||||
:remainder-id :rest}))
|
||||
(for [at (range -1 15)] #(span/place-symbol % nil :main :n :wave at
|
||||
{:extent :grow-symbol
|
||||
:remainder-id :rest}))
|
||||
(for [at (range -1 15)] #(span/adopt % :main :b at {:extent :grow-symbol
|
||||
:remainder-id :rest}))
|
||||
[#(span/make-unique (:clip (span/reuse-drawing % :main :n :drawing-a
|
||||
{:extent :grow-symbol}))
|
||||
:main :n {})])
|
||||
results (keep (fn [command] (:clip (command doc))) every-command)]
|
||||
(is (< 190 (count results)) "the sample is of commands that actually did something")
|
||||
(doseq [after results]
|
||||
(is (empty? (symbol/overlaps (get-in after [:symbols :main])))
|
||||
(str "a command committed an overlap: "
|
||||
(pr-str (mapv (juxt :id node/placed-span)
|
||||
(symbol/children (get-in after [:symbols :main :nodes]))))))
|
||||
(is (empty? (clip/problems after))))))
|
||||
|
||||
(deftest turning-lane-mode-on-is-the-one-thing-that-can-be-refused
|
||||
(let [composed (-> (document)
|
||||
(update-in [:symbols :main] dissoc :display)
|
||||
(assoc-in [:symbols :main :nodes :b :time :at] 3))
|
||||
refused (span/draw-as-lane composed :main true {})
|
||||
trimmed (:clip (span/draw-as-lane composed :main true {:trim? true}))]
|
||||
(is (:refused refused))
|
||||
(is (= 1 (:required-trim refused)))
|
||||
(is (nil? (:clip refused)) "and nothing moved")
|
||||
(is (= :lane (get-in trimmed [:symbols :main :display])))
|
||||
(is (= [[0 3] [3 7] [8 12]]
|
||||
(mapv node/placed-span (symbol/children (get-in trimmed [:symbols :main :nodes]))))
|
||||
"the retry trims later-claims-from-earlier, the rule everything else follows")
|
||||
(is (empty? (symbol/overlaps (get-in trimmed [:symbols :main]))))
|
||||
(is (empty? (clip/problems trimmed)))
|
||||
;; A clip the next one wholly covers has nothing left to be.
|
||||
(let [buried (assoc-in composed [:symbols :main :nodes :b :time :at] 0)
|
||||
after (:clip (span/draw-as-lane buried :main true {:trim? true}))]
|
||||
(is (nil? (get-in after [:symbols :main :nodes :a])))
|
||||
(is (empty? (symbol/overlaps (get-in after [:symbols :main])))))
|
||||
;; Off is always possible, and changes nothing but the hint.
|
||||
(let [off (:clip (span/draw-as-lane (document) :main false {}))]
|
||||
(is (nil? (get-in off [:symbols :main :display])))
|
||||
(is (= (get-in (document) [:symbols :main :nodes])
|
||||
(get-in off [:symbols :main :nodes]))))
|
||||
(is (:refused (span/draw-as-lane (document) :nothing-here true {})))
|
||||
(is (= :lane (get-in (:clip (span/draw-as-lane (document) :main true {}))
|
||||
[:symbols :main :display]))
|
||||
"a sequence that is already one turns on with nothing to trim")))
|
||||
|
||||
(deftest playback-is-independent-of-property-channel-shape
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :a])
|
||||
keyed (node/toggle-key n [:xform :rot] 0 nil)
|
||||
unkeyed (node/toggle-key keyed [:xform :rot] 0 nil)]
|
||||
(doseq [n [n keyed unkeyed]]
|
||||
(is (= {:symbol :drawing-a :frame 0} (node/placed-frame n 11 1))))
|
||||
(let [n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= {:symbol :wave :frame 5} (node/placed-frame n 2 10)))
|
||||
(is (nil? (node/placed-frame n 7 10)))
|
||||
(is (= 9 (:frame (node/placed-frame (assoc-in n [:playback :end] :hold) 9 10))))
|
||||
(is (= 2 (:frame (node/placed-frame (assoc-in n [:playback :end] :loop) 9 10)))))))
|
||||
|
||||
(deftest navigation-and-hit-testing-use-the-same-source-time
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= 5 (:frame (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= {:at 5 :rate 1} (:time (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
||||
(is (= ((pick/bounds-of doc nil :main n) 2)
|
||||
((pick/bounds-of doc nil :main (assoc-in n [:playback :in] 5)) 0)))))
|
||||
|
||||
(deftest seeking-and-source-reuse-do-not-share-cursors
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
||||
fs [11 0 5 3 8 4 10 1 6 2 9 7]
|
||||
at (sample doc fs)]
|
||||
(is (= at (sample doc (reverse fs))))
|
||||
(is (= at (sample doc (range 12))))
|
||||
(let [edited (assoc-in doc [:symbols :drawing-a :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [99 0]))]
|
||||
(is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]])))
|
||||
(is (= 99 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
|
||||
|
||||
(deftest cel-identities-and-playback-round-trip
|
||||
(let [doc (:clip (span/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
|
||||
leaves (leaf/leaves :project doc)]
|
||||
(is (= doc (leaf/clip :project leaves)))
|
||||
(is (contains? leaves "clip/project/symbol/main/node/a"))
|
||||
(is (= :lane (get-in (leaf/clip :project leaves) [:symbols :main :display]))
|
||||
"lane mode is saved like any other field of a symbol")
|
||||
(let [{copied :clip ids :ids}
|
||||
(bring/symbols (assoc-in (clip/blank) [:symbols :drawing-a] (drawing :drawing-a 99 1))
|
||||
doc [:main] {})]
|
||||
(is (= :drawing-a-2 (:drawing-a ids)))
|
||||
(is (= #{:drawing-a-2 :drawing-b :wave} (clip/places copied (:main ids))))
|
||||
(is (empty? (clip/problems copied))))))
|
||||
|
||||
(deftest one-transaction-undoes-the-ripple-and-shot-extension
|
||||
(let [doc (document)
|
||||
after (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
before-leaves (leaf/leaves :p doc)
|
||||
after-leaves (leaf/leaves :p after)
|
||||
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
|
||||
undo (history/undo h after-leaves)
|
||||
redo (history/redo (:history undo) (:leaves undo))]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= before-leaves (:leaves undo)))
|
||||
(is (= after-leaves (:leaves redo)))))
|
||||
|
||||
(deftest a-new-document-is-ordinary-and-a-lane-is-created-explicitly
|
||||
(let [doc (clip/blank)
|
||||
lane (:clip (span/draw-as-lane doc :main true {}))
|
||||
a (:clip (span/append-drawing lane :main :a :drawing-a {}))
|
||||
b (:clip (span/append-drawing a :main :b :drawing-b {}))]
|
||||
(is (= {} (get-in doc [:symbols :main :nodes])))
|
||||
(is (nil? (get-in doc [:symbols :main :display])))
|
||||
(is (= :lane (get-in lane [:symbols :main :display])))
|
||||
(is (empty? (clip/problems b)))
|
||||
(is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b]))))
|
||||
(is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed])))
|
||||
(is (:refused (span/append-drawing b :main :a :new {})))))
|
||||
|
||||
(deftest arbitrary-symbols-drop-into-the-sequence-and-claim-their-time
|
||||
(let [doc (document)
|
||||
dropped (span/place-symbol doc nil :main :clip :wave 2
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip dropped)
|
||||
clips (symbol/children (get-in after [:symbols :main :nodes]))]
|
||||
(is (= :clip (:selection dropped)))
|
||||
(is (= [[0 2] [2 12]] (mapv node/placed-span clips))
|
||||
"the natural ten-frame symbol claims [2,12), trimming/removing incumbents")
|
||||
(is (= :wave (node/source (second clips))))
|
||||
(is (= 1 (:speed (node/playback-of (second clips))))
|
||||
"a dropped symbol plays; it is not converted into a drawing hold")
|
||||
(is (empty? (clip/problems after)))
|
||||
;; Outside lane mode the same drop claims nothing.
|
||||
(let [composed (update-in doc [:symbols :main] dissoc :display)
|
||||
after (:clip (span/place-symbol composed nil :main :clip :wave 2
|
||||
{:extent :grow-symbol :remainder-id :tail}))]
|
||||
(is (= [[0 4] [2 12] [4 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/children (get-in after [:symbols :main :nodes]))))
|
||||
"it is simply placed, on screen with what was already there"))))
|
||||
|
||||
(deftest an-existing-clip-can-be-moved-and-claim-where-it-lands
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :badge]
|
||||
{:id :badge :kind :instance :z "z"
|
||||
:source {:symbol :wave} :span [0 3]
|
||||
:time {:at 1 :rate 1}
|
||||
:playback {:in 2 :speed 1 :end :stop}})
|
||||
result (span/adopt doc :main :badge 5
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :badge])]
|
||||
(is (= [5 8] (node/placed-span n)))
|
||||
(is (= {:in 2 :speed 1 :end :stop} (:playback n))
|
||||
"a move changes placement, not source timing")
|
||||
(is (= [[0 4] [4 5] [5 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/children (get-in after [:symbols :main :nodes])))))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (:refused (span/adopt doc :main :plate 5 {})) "and a shape has no frames to place")))
|
||||
|
||||
(deftest fractional-placement-rates-convert-the-hold-delta
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :main :nodes :a :time :rate] 2)
|
||||
(assoc-in [:symbols :main :nodes :a :span] [0 8]))
|
||||
after (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
(is (= [0 12] (get-in after [:symbols :main :nodes :a :span])))
|
||||
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b]))))))
|
||||
|
||||
(deftest bare-shapes-agree-in-reference-and-playback
|
||||
(let [sym (drawing :bare 12 1)]
|
||||
(is (= (symbol/eval-frame sym 0 nil pal/index-of nil)
|
||||
((symbol/resolver sym nil pal/index-of nil) 0))
|
||||
"omitted style colour must not crash a missing cursor")))
|
||||
|
||||
(deftest audio-follows-only-the-playing-cel
|
||||
(let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"}
|
||||
:span [0 10]
|
||||
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
|
||||
doc (-> (document)
|
||||
(assoc-in [:symbols :wave :nodes :voice] voice)
|
||||
(assoc-in [:symbols :drawing-a :nodes :voice] voice))
|
||||
[track :as tracks] (nest/audio-tracks doc :main)]
|
||||
(is (= 1 (count tracks)) "the frozen drawing contributes no audio")
|
||||
(is (= [8 12] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track)) "the source in-point trims the audio too")
|
||||
(is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys])))
|
||||
(let [moved (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
[track] (nest/audio-tracks moved :main)]
|
||||
(is (= [10 14] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track))))
|
||||
;; The retime that used to be the lane group's is the placing instance's.
|
||||
(let [fast (-> (shot doc)
|
||||
(assoc-in [:symbols :shot :nodes :girl :time] {:mode :map :at 2 :rate 2})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback :speed] 2))
|
||||
[track] (nest/audio-tracks fast :shot)]
|
||||
(is (= [6 7.75] (node/placed-span track)))
|
||||
(is (= [3 10] (:span track)))
|
||||
(is (= 4 (get-in track [:time :rate]))))))
|
||||
|
||||
(deftest a-looped-insert-schedules-distinct-audio-intervals
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :wave :frames] 4)
|
||||
(assoc-in [:symbols :wave :nodes :voice]
|
||||
{:id :voice :kind :audio :z "a" :source {:sound "v"} :span [1 3]})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback]
|
||||
{:in 3 :speed 1 :end :loop}))]
|
||||
(is (= [[10 12]] (mapv node/placed-span (nest/audio-tracks doc :main))))))
|
||||
|
||||
(deftest the-shot-a-sequence-needs-is-in-its-own-frames
|
||||
;; CHANGED ON PURPOSE. It used to be that an enclosing retime changed how many
|
||||
;; frames an edit needed, because the lane was INSIDE the symbol being measured
|
||||
;; and its clock sat between them. The symbol is now the container, so its
|
||||
;; `:frames` is its own authored window and how fast some instance plays it is
|
||||
;; not a fact about it.
|
||||
(let [doc (shot (document))
|
||||
slow (assoc-in doc [:symbols :shot :nodes :girl :time] {:mode :map :at 8 :rate 2})]
|
||||
(is (= 14 (:required-frames (span/extend-hold slow :main :a 2 {}))))
|
||||
(is (= 14 (:required-frames (span/extend-hold doc :main :a 2 {}))))
|
||||
(is (= 14 (get-in (span/extend-hold slow :main :a 2 {:extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))))
|
||||
|
||||
(deftest reuse-shares-content-and-make-unique-decouples-one-cel
|
||||
(let [doc (document)
|
||||
shared (:clip (span/reuse-drawing doc :main :c :drawing-a {:extent :grow-symbol}))
|
||||
edit (fn [c sym x]
|
||||
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [x 0])))]
|
||||
(is (:refused (span/reuse-drawing doc :main :c :drawing-a {}))
|
||||
"the shot has to be extended on purpose")
|
||||
(is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (empty? (clip/problems shared)))
|
||||
;; One drawing, two cels: the edit arrives at both.
|
||||
(let [at (sample (edit shared :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 99 (get-in at [12 [:c :mark]]))))
|
||||
(let [unique (:clip (span/make-unique shared :main :c {}))]
|
||||
(is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c]))))
|
||||
(is (= (:nodes (get-in shared [:symbols :drawing-a]))
|
||||
(:nodes (get-in unique [:symbols :drawing-a-2])))
|
||||
"a copy of the same drawing, not an empty one")
|
||||
(is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a])))
|
||||
"the other cel keeps the original")
|
||||
(let [at (sample (edit unique :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 10 (get-in at [12 [:c :mark]])) "the cel made unique is untouched"))
|
||||
(let [at (sample (edit unique :drawing-a-2 99) [0 12])]
|
||||
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
|
||||
(is (empty? (clip/problems unique))))
|
||||
;; Nothing else places drawing-b, so there is nothing to decouple from.
|
||||
(is (:refused (span/make-unique doc :main :b {})))
|
||||
(is (:refused (span/make-unique doc :main :plate {}))
|
||||
"a shape places nothing itself")))
|
||||
|
||||
(deftest duplicate-copies-the-drawing-and-not-the-cel
|
||||
(let [doc (document)
|
||||
made (:clip (span/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
|
||||
n (get-in made [:symbols :main :nodes :d])]
|
||||
(is (= :drawing-b-2 (node/source n)))
|
||||
(is (= (:nodes (get-in doc [:symbols :drawing-b]))
|
||||
(:nodes (get-in made [:symbols :drawing-b-2]))))
|
||||
(is (= [12 13] (node/placed-span n)))
|
||||
(is (= {:in 0 :speed 0 :end :stop} (:playback n)))
|
||||
(is (nil? (:channels n)) "B's own position correction belongs to B's cel")
|
||||
(is (= (get-in doc [:symbols :main :nodes :b])
|
||||
(get-in made [:symbols :main :nodes :b]))
|
||||
"the drawing duplicated is left as it was")
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest a-shallow-copy-keeps-its-parts-and-a-deep-copy-owns-them
|
||||
;; A drawing assembled from another symbol: copying it shallowly must keep
|
||||
;; using that part, and only an explicit deep copy may promise independence.
|
||||
(let [doc (assoc-in (document) [:symbols :drawing-a :nodes :part]
|
||||
{:id :part :kind :instance :z "b" :span [0 1]
|
||||
:time {:at 0 :rate 1} :source {:symbol :wave}
|
||||
:playback {:in 0 :speed 0 :end :stop}})
|
||||
copy (fn [opts] (:clip (span/duplicate-drawing
|
||||
doc :main :a :d (merge {:extent :grow-symbol} opts))))
|
||||
shallow (copy {})
|
||||
deep (copy {:deep? true})]
|
||||
(is (= :wave (node/source (get-in shallow [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (nil? (get-in shallow [:symbols :wave-2])))
|
||||
(is (= :wave-2 (node/source (get-in deep [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (= (:nodes (get-in doc [:symbols :wave])) (:nodes (get-in deep [:symbols :wave-2]))))
|
||||
(is (empty? (clip/problems shallow)))
|
||||
(is (empty? (clip/problems deep)))))
|
||||
|
||||
(deftest reuse-refuses-what-would-not-be-a-document
|
||||
(let [doc (document)]
|
||||
(is (:refused (span/reuse-drawing doc :main :c :nothing-here {})))
|
||||
(is (:refused (span/reuse-drawing doc :main :c :main {:extent :grow-symbol}))
|
||||
"a symbol cannot go inside itself")
|
||||
(is (:refused (span/reuse-drawing doc :main :a :drawing-a {:extent :grow-symbol}))
|
||||
"a cel ID in use is not free")
|
||||
(is (:refused (span/duplicate-drawing doc :main :plate :d {})))))
|
||||
|
||||
(deftest drawing-on-twos-does-not-quantize-the-containers-transform
|
||||
;; Cel length IS the drawing cadence, and it is the only thing on twos here:
|
||||
;; the instance placing the sequence has its own clock and keeps moving every
|
||||
;; frame. Stepping it would be the cel cadence leaking into continuous motion.
|
||||
(let [cel (fn [id source at] (cel id source at 2 0))
|
||||
doc (-> (shot (document))
|
||||
(update-in [:symbols :main :nodes] dissoc :a :b :insert)
|
||||
(update-in [:symbols :main :nodes] merge
|
||||
{:c0 (cel :c0 :drawing-a 0)
|
||||
:c1 (cel :c1 :drawing-b 2)
|
||||
:c2 (cel :c2 :drawing-a 4)}))
|
||||
xs {:c0 10 :c1 20 :c2 10}
|
||||
at (sample-in doc :shot (range 6))
|
||||
showing (fn [f] (first (dissoc (at f) :plate)))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= [:c0 :c0 :c1 :c1 :c2 :c2] (mapv #(second (key (showing %))) (range 6)))
|
||||
"the drawing showing changes every second frame")
|
||||
(is (= [0 10 20 30 40 50]
|
||||
(mapv (fn [f] (let [[[_ id _] cx] (showing f)] (- cx (xs id)))) (range 6)))
|
||||
"and the sequence moves on every frame, odd ones included")))
|
||||
|
||||
(defn- drawn
|
||||
"What every frame draws, as sorted values, so a picture can be compared
|
||||
without naming the cels that produced it."
|
||||
[doc sid fs]
|
||||
(let [at (sample-in doc sid fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
||||
(deftest a-drawing-goes-anywhere-in-the-sequence-and-ripples-what-follows
|
||||
(let [doc (shot (document))
|
||||
keys-of #(get-in % [:symbols :shot :nodes :girl :channels [:xform :pos] :keys])
|
||||
spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id])))
|
||||
[:a :n :b :insert])
|
||||
r (span/append-drawing doc :main :n :drawing-n {:at 4 :extent :grow-symbol})]
|
||||
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
|
||||
(is (= 13 (get-in r [:clip :symbols :main :frames])))
|
||||
(is (= (keys-of doc) (keys-of (:clip r)))
|
||||
"the container's keys stay where they were authored")
|
||||
(is (= :n (:selection r)))
|
||||
(is (= 4 (:frame r)))
|
||||
(is (empty? (clip/problems (:clip r))))
|
||||
;; The same command with no room refuses, and says how much it needs.
|
||||
(is (= 13 (:required-frames (span/append-drawing doc :main :n :drawing-n {:at 4}))))
|
||||
;; At the very front everything moves.
|
||||
(is (= [[1 5] [0 1] [5 9] [9 13]]
|
||||
(spans (:clip (span/append-drawing doc :main :n :drawing-n
|
||||
{:at 0 :extent :grow-symbol})))))
|
||||
;; Inside a cel is not a position for another one.
|
||||
(is (re-find #"split it first"
|
||||
(:refused (span/append-drawing doc :main :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol}))))
|
||||
(is (:refused (span/append-drawing doc :main :n :drawing-n
|
||||
{:at -1 :extent :grow-symbol})))
|
||||
(is (:refused (span/append-drawing doc :main :n :drawing-n
|
||||
{:at ##Inf :extent :grow-symbol})))
|
||||
;; Reuse and duplicate take a position too; it is one placement rule.
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (span/reuse-drawing doc :main :n :drawing-b
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (span/duplicate-drawing doc :main :b :n
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))))
|
||||
|
||||
(deftest split-then-place-puts-a-drawing-inside-a-hold
|
||||
;; The two commands the doc asks for, composed: neither one guesses.
|
||||
(let [doc (shot (document))
|
||||
cut (:clip (span/split doc :main :a 2 :right))
|
||||
r (span/append-drawing cut :main :n :drawing-n {:at 2 :extent :grow-symbol})
|
||||
after (:clip r)]
|
||||
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
|
||||
(mapv #(node/placed-span (get-in after [:symbols :main :nodes %]))
|
||||
[:a :n :right :b :insert])))
|
||||
(is (= (get-in doc [:symbols :shot :nodes :girl :channels])
|
||||
(get-in after [:symbols :shot :nodes :girl :channels]))
|
||||
"the performance is still timed the way it was authored")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-three-frame-correction-crosses-a-drawing-boundary
|
||||
;; The lane model's worked example, with the correction on the INSTANCE that
|
||||
;; places the sequence: it applies across whichever drawings are showing under
|
||||
;; it, and outside its three frames the animation evaluates exactly as before.
|
||||
(let [doc (shot (document))
|
||||
fs (range 12)
|
||||
before (drawn doc :shot fs)
|
||||
beat (ch/layer :beat [3 6] :offset (ch/framed [30 0]))
|
||||
c (update-in doc [:symbols :shot :nodes :girl :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
after (drawn c :shot fs)
|
||||
outside [0 1 2 6 7 8 9 10 11]]
|
||||
(is (empty? (clip/problems c)))
|
||||
(is (= (mapv before outside) (mapv after outside))
|
||||
"outside the support, frame for frame identical")
|
||||
(let [at (sample-in c :shot [3 4 5])]
|
||||
;; Frame 3 shows drawing A and frames 4 and 5 show drawing B: one
|
||||
;; correction, reaching across the cut between them.
|
||||
(is (= 70 (get-in at [3 [:girl :a :mark]])))
|
||||
(is (= 90 (get-in at [4 [:girl :b :mark]])))
|
||||
(is (= 102 (get-in at [5 [:girl :b :mark]]))
|
||||
"and B's own correction still applies under it")
|
||||
(is (= [-10 0 10] (mapv (fn [f] (js/Math.round (get-in at [f :plate]))) [3 4 5]))
|
||||
"while the background, which is not in the sequence, does not move"))
|
||||
;; One document change: one step, and it persists in the channel's own leaf.
|
||||
(let [b (leaf/leaves :p doc)
|
||||
a (leaf/leaves :p c)
|
||||
h (-> nil history/hold (history/record b a 0) history/settle)]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= b (:leaves (history/undo h a))))
|
||||
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
|
||||
|
||||
(deftest a-correction-on-one-cel-travels-with-it
|
||||
;; The other half of ownership: a layer on a cel is in that cel's own frames,
|
||||
;; so moving the cel moves the correction and nothing has to say so.
|
||||
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
|
||||
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
moved (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
;; Stated as the difference from the same document without the correction,
|
||||
;; so the claim is about WHERE the layer applies and not about arithmetic.
|
||||
(let [nudge (fn [with without f]
|
||||
(- (get-in (sample with [f]) [f [:b :mark]])
|
||||
(get-in (sample without [f]) [f [:b :mark]])))]
|
||||
(is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7]))
|
||||
"B's first two frames, 4 and 5")
|
||||
(is (= [7 7 0 0]
|
||||
(mapv #(nudge moved (:clip (span/extend-hold (document) :main :a 2
|
||||
{:extent :grow-symbol}))
|
||||
%)
|
||||
[6 7 8 9]))
|
||||
"and after A's hold grows, B's first two frames, which are now 6 and 7"))
|
||||
(is (= (get-in doc [:symbols :main :nodes :b :channels])
|
||||
(get-in moved [:symbols :main :nodes :b :channels]))
|
||||
"the layer itself was not touched by the retiming")
|
||||
(is (empty? (clip/problems moved)))))
|
||||
|
||||
(defn- spans [clip ids]
|
||||
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
|
||||
|
||||
(deftest blanking-leaves-a-gap-and-does-not-close-it
|
||||
(let [doc (document)
|
||||
r (span/blank doc :main [5 7] {:id :rest})
|
||||
after (:clip r)]
|
||||
;; B spanned the range, so it became two cels with a hole between them.
|
||||
(is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert])))
|
||||
(is (= :rest (:selection r)))
|
||||
(let [at (sample after [4 5 6 7])]
|
||||
(is (= #{:plate} (set (keys (at 5)))) "nothing is drawn on a blanked frame")
|
||||
(is (= #{:plate} (set (keys (at 6)))))
|
||||
(is (get-in at [4 [:b :mark]]))
|
||||
(is (get-in at [7 [:rest :mark]])))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-whole-cel-removes-it-and-keeps-its-drawing
|
||||
(let [doc (document)
|
||||
r (span/blank doc :main [4 8] {})
|
||||
after (:clip r)]
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
|
||||
(is (nil? (:selection r))
|
||||
"and names nothing, because emptying frames selects nothing sensible")
|
||||
(is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b]))
|
||||
"a symbol does not own its content")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-range-trims-what-it-only-partly-covers
|
||||
(let [doc (document)
|
||||
after (:clip (span/blank doc :main [3 9] {}))]
|
||||
(is (= [[0 3] [9 12]] (spans after [:a :insert])))
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= (get-in (sample doc [9]) [9 [:insert :mark]])
|
||||
(get-in (sample after [9]) [9 [:insert :mark]]))
|
||||
"the insert kept its own frames, so frame 9 shows what it showed")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest overwrite-clears-one-frame-and-does-not-ripple-what-follows
|
||||
(let [r (span/overwrite-drawing (document) :main :n :drawing-n 5
|
||||
{:extent :keep :remainder-id :right})
|
||||
after (:clip r)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :n (:selection r)))
|
||||
(is (= [[0 4] [4 5] [5 6] [6 8] [8 12]]
|
||||
(mapv #(node/placed-span (get nodes %)) [:a :b :n :right :insert])))
|
||||
(is (= :drawing-b (node/source (:right nodes))))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
||||
(let [doc (document)]
|
||||
(is (re-find #"free ID" (:refused (span/blank doc :main [5 7] {})))
|
||||
"splitting a cel needs an ID for the remainder")
|
||||
(is (:refused (span/blank doc :main [5 7] {:id :a})) "and a free one")
|
||||
(is (:refused (span/blank doc :main [7 5] {})))
|
||||
(is (:refused (span/blank doc :main [5 5] {})))
|
||||
(is (:refused (span/blank doc :main [5 6.5] {})))
|
||||
(is (:refused (span/blank (update-in doc [:symbols :main] dissoc :display)
|
||||
:main [0 2] {}))
|
||||
"and a composition has no sequence to leave a hole in")))
|
||||
|
||||
(deftest the-shot-length-is-authored-and-emptying-it-does-not-shorten-it
|
||||
;; The window and the occupied extent are two facts. A shot with nothing in
|
||||
;; the last half is a shot somebody authored that long, and deleting the last
|
||||
;; drawing must not quietly shorten the film.
|
||||
(let [doc (document)
|
||||
emptied (:clip (span/blank doc :main [0 12] {}))]
|
||||
(is (empty? (symbol/children (get-in emptied [:symbols :main :nodes]))))
|
||||
(is (= 12 (get-in emptied [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems emptied)))
|
||||
;; Growing is still the caller's word, and only ever grows.
|
||||
(is (:refused (span/append-drawing emptied :main :n :drawing-n {:at 20})))
|
||||
(is (= 21 (get-in (span/append-drawing emptied :main :n :drawing-n
|
||||
{:at 20 :extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))
|
||||
(is (= 12 (get-in (:clip (span/trim doc :main :insert :out 9))
|
||||
[:symbols :main :frames]))
|
||||
"and trimming the last cel leaves the window where it was")))
|
||||
|
||||
(deftest a-take-placed-in-a-sequence-is-still-heard
|
||||
;; `bring/take` puts a take's sound INSIDE the symbol it makes, so that
|
||||
;; "wherever the symbol is placed it is heard". A sequence is one of the places
|
||||
;; it can be placed, and must not be the one place that goes silent.
|
||||
(let [doc (assoc-in (document) [:symbols :take]
|
||||
{:id :take :frames 10 :fps 24
|
||||
:nodes {:pic {:id :pic :kind :instance :z "a"
|
||||
:source {:symbol :wave} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:playback {:in 0 :speed 1 :end :stop}}
|
||||
:sound {:id :sound :name "sound" :kind :audio
|
||||
:parent nil :z "z-sound"
|
||||
:source {:footage "f1"} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}}}})
|
||||
at-root (clip/place-symbol doc nil :main :take 0 :root nil)
|
||||
placed (:clip (span/place-symbol doc nil :main :drop :take 0
|
||||
{:extent :grow-symbol :remainder-id :tail}))]
|
||||
(is (= 1 (count (nest/audio-tracks at-root :main)))
|
||||
"a take placed at the root is heard")
|
||||
(is (some? placed) "the take goes into the sequence")
|
||||
(is (= 1 (count (nest/audio-tracks placed :main)))
|
||||
"and is still heard from inside one")))
|
||||
|
||||
(deftest a-sound-is-a-clip-of-a-sequence-like-any-other
|
||||
;; A sound claims time by the same rule as a picture, and THERE IS NO MIXTURE
|
||||
;; RULE ANY MORE: a symbol whose clips are sounds is an audio lane, and that is
|
||||
;; the whole of it. The refusal that used to say "picture or sound, not both"
|
||||
;; was a property of a lane node, and there is no lane node.
|
||||
(let [seeded (clip/place-sound (document) :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
result (span/adopt seeded :main :vo 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :vo])]
|
||||
(is (nil? (:refused result)) (str (:refused result)))
|
||||
(is (= [2 8] (node/placed-span n)))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= 1 (count (nest/audio-tracks after :main)))
|
||||
"a sound in a sequence is still heard")
|
||||
(is (contains? (set (map :id (symbol/children (get-in after [:symbols :main :nodes]))))
|
||||
:vo))
|
||||
;; Picture over sound is picture claiming the frames, like anything else.
|
||||
(let [mixed (:clip (span/place-symbol after nil :main :also :wave 2
|
||||
{:extent :grow-symbol :remainder-id :rest}))]
|
||||
(is (empty? (symbol/overlaps (get-in mixed [:symbols :main]))))
|
||||
(is (empty? (clip/problems mixed))))))
|
||||
|
|
@ -1,53 +1,22 @@
|
|||
(ns arthur.domain.span-test
|
||||
"Split, trim and move, over the two things they have to work on alike: a cel
|
||||
inside a lane, and a symbol placed straight into a shot. The fixture carries
|
||||
both on purpose — the commands were lane-gated for as long as a lane was the
|
||||
only thing anybody had timed, and the point of these tests is that nothing in
|
||||
them reads a lane."
|
||||
"Generic span edits in an explicit lane and an ordinary compositing symbol."
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.sequence-test :as fixture]
|
||||
[arthur.domain.span :as span]))
|
||||
|
||||
(defn- drawing [id x frames]
|
||||
{:id id :frames frames
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 4)
|
||||
[:xform :pos] (ch/framed [x 0])}}}})
|
||||
(defn document [] (fixture/document))
|
||||
|
||||
(defn- cel [id source at duration speed]
|
||||
{:id id :kind :instance :parent :girl :z (name id)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document
|
||||
"A lane of three cels, and — the part lane_test's fixture has no equivalent of
|
||||
— `:badge`, an instance of an animated symbol placed straight into `:main`
|
||||
with a span of its own and no parent at all. Its frames are the SYMBOL's, so
|
||||
it is the case where the coordinate a command takes is not lane time."
|
||||
[]
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (cel :b :drawing-b 4 4 0)
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "spans" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12
|
||||
:nodes {:girl {:id :girl :kind :group :layout :sequence :z "b"}
|
||||
:a a :b b :insert insert
|
||||
:badge {:id :badge :kind :instance :z "c"
|
||||
:source {:symbol :wave}
|
||||
:playback {:in 0 :speed 1 :end :stop}
|
||||
:time {:at 2 :rate 1} :span [0 8]}
|
||||
:plate {:id :plate :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 10)
|
||||
[:xform :pos] (ch/keyed {0 [-40 0] 11 [70 0]} :linear)}}}}
|
||||
:drawing-a (drawing :drawing-a 10 1)
|
||||
:drawing-b (drawing :drawing-b 20 1)
|
||||
:wave (assoc-in (drawing :wave 0 10) [:nodes :mark :channels [:xform :pos]]
|
||||
(ch/keyed {0 [0 0] 9 [900 0]} :linear))}}))
|
||||
(defn- ordinary-document []
|
||||
(-> (document)
|
||||
(update-in [:symbols :main] dissoc :display)
|
||||
(assoc-in [:symbols :main :nodes :badge]
|
||||
{:id :badge :kind :instance :z "z"
|
||||
:source {:symbol :wave}
|
||||
:playback {:in 0 :speed 1 :end :stop}
|
||||
:time {:at 2 :rate 1} :span [0 8]})))
|
||||
|
||||
(defn- sample [doc fs]
|
||||
(let [r (clip/resolver doc :main nil pal/index-of nil)]
|
||||
|
|
@ -57,172 +26,67 @@
|
|||
(let [at (sample doc fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
||||
(defn- spans [clip ids]
|
||||
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
|
||||
(deftest fixtures-state-the-mode-explicitly
|
||||
(is (= :lane (get-in (document) [:symbols :main :display])))
|
||||
(is (nil? (get-in (ordinary-document) [:symbols :main :display])))
|
||||
(is (empty? (clip/problems (document))))
|
||||
(is (empty? (clip/problems (ordinary-document)))))
|
||||
|
||||
(deftest the-fixture-places-one-thing-outside-the-lane
|
||||
(deftest splitting-preserves-the-picture-in-both-modes
|
||||
(doseq [[label doc id cut] [["lane clip" (document) :a 2]
|
||||
["ordinary placement" (ordinary-document) :badge 6]]]
|
||||
(testing label
|
||||
(let [before (drawn doc (range 12))
|
||||
r (span/split doc :main id cut :right)
|
||||
after (:clip r)]
|
||||
(is (= :right (:selection r)))
|
||||
(is (= before (drawn after (range 12))))
|
||||
(is (= cut
|
||||
(second (node/placed-span (get-in after [:symbols :main :nodes id])))
|
||||
(first (node/placed-span (get-in after [:symbols :main :nodes :right])))))
|
||||
(is (empty? (clip/problems after)))))))
|
||||
|
||||
(deftest split-refuses-an-edge-a-missing-node-and-a-spanless-node
|
||||
(let [doc (document)]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (nil? (:parent (get-in doc [:symbols :main :nodes :badge])))
|
||||
"so a command acting on it has only the symbol's frames to go by")
|
||||
(is (= [2 10] (node/placed-span (get-in doc [:symbols :main :nodes :badge]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; split
|
||||
|
||||
(deftest splitting-changes-nothing-that-is-drawn
|
||||
(let [doc (document)
|
||||
fs (range 12)
|
||||
before (drawn doc fs)]
|
||||
(doseq [[label id cut] [["a held drawing in a lane" :a 2]
|
||||
["a playing insert in a lane" :insert 10]
|
||||
["a placement with no lane at all" :badge 6]]]
|
||||
(testing label
|
||||
(let [r (span/split doc :main id cut :right)
|
||||
after (:clip r)]
|
||||
(is (= :right (:selection r)))
|
||||
(is (= before (drawn after fs)) "the same picture, frame for frame")
|
||||
(is (= (node/placed-span (get-in doc [:symbols :main :nodes id]))
|
||||
[(first (node/placed-span (get-in after [:symbols :main :nodes id])))
|
||||
(second (node/placed-span (get-in after [:symbols :main :nodes :right])))])
|
||||
"the pieces occupy the frames the one node did")
|
||||
(is (= cut (second (node/placed-span (get-in after [:symbols :main :nodes id])))
|
||||
(first (node/placed-span (get-in after [:symbols :main :nodes :right])))))
|
||||
(is (= (:time (get-in doc [:symbols :main :nodes id]))
|
||||
(:time (get-in after [:symbols :main :nodes :right])))
|
||||
"one time map, so the right piece's own frames carry on")
|
||||
(is (= (select-keys (get-in doc [:symbols :main :nodes id])
|
||||
[:source :playback :channels :parent :z])
|
||||
(select-keys (get-in after [:symbols :main :nodes :right])
|
||||
[:source :playback :channels :parent :z]))
|
||||
"and it keeps its parent and its depth, so it draws where it drew")
|
||||
(is (= 12 (get-in after [:symbols :main :frames])) "and no shot-length question")
|
||||
(is (empty? (clip/problems after))))))))
|
||||
|
||||
(deftest split-refuses-anything-but-one-cut-inside-one-thing
|
||||
(let [doc (document)]
|
||||
(doseq [cut [0 4 8 12 -1 2.5 ##NaN nil]]
|
||||
(is (:refused (span/split doc :main :b cut :right)) (str "cut at " (pr-str cut))))
|
||||
(is (:refused (span/split doc :main :a 2 :b)) "the new ID has to be free")
|
||||
(doseq [cut [0 4 -1 2.5 ##NaN nil]]
|
||||
(is (:refused (span/split doc :main :a cut :right))))
|
||||
(is (:refused (span/split doc :main :missing 2 :right)))
|
||||
(is (re-find #"group" (:refused (span/split doc :main :girl 2 :right)))
|
||||
"a group is divided by its children, not by its span")
|
||||
(is (re-find #"whole shot" (:refused (span/split doc :main :plate 2 :right)))
|
||||
"and a node with no span has no edges to cut")))
|
||||
(is (re-find #"whole shot" (:refused (span/split doc :main :plate 2 :right))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; trim
|
||||
(deftest trimming-only-narrows-the-selected-placement
|
||||
(doseq [[doc id edge to kept] [[(document) :b :out 6 [4 6]]
|
||||
[(ordinary-document) :badge :in 5 [5 10]]]]
|
||||
(let [before (get-in doc [:symbols :main :nodes id])
|
||||
r (span/trim doc :main id edge to)
|
||||
after (:clip r)]
|
||||
(is (= id (:selection r)))
|
||||
(is (= kept (node/placed-span (get-in after [:symbols :main :nodes id]))))
|
||||
(is (= (dissoc before :span)
|
||||
(dissoc (get-in after [:symbols :main :nodes id]) :span)))
|
||||
(is (empty? (clip/problems after))))))
|
||||
|
||||
(deftest trimming-narrows-one-thing-and-moves-nothing-else
|
||||
(let [doc (document)]
|
||||
(doseq [[label id edge to kept] [["a cel in a lane" :b :out 6 [4 6]]
|
||||
["a placement outside one" :badge :out 7 [2 7]]
|
||||
["the front of one outside a lane" :badge :in 5 [5 10]]]]
|
||||
(testing label
|
||||
(let [r (span/trim doc :main id edge to)
|
||||
after (:clip r)]
|
||||
(is (= kept (node/placed-span (get-in after [:symbols :main :nodes id]))))
|
||||
(is (= id (:selection r)))
|
||||
(is (= (select-keys (get-in doc [:symbols :main :nodes id])
|
||||
[:time :playback :channels :source])
|
||||
(select-keys (get-in after [:symbols :main :nodes id])
|
||||
[:time :playback :channels :source]))
|
||||
"only :span changed")
|
||||
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and no neighbour moved")
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after))))))))
|
||||
|
||||
(deftest trimming-the-front-does-not-restart-what-is-playing
|
||||
;; The difference between trimming and slipping, asserted on the node that has
|
||||
;; no lane: its own frames are where they were, so the frames that survive
|
||||
;; show exactly what they showed.
|
||||
(let [doc (document)
|
||||
before (sample doc [6 7])
|
||||
after (:clip (span/trim doc :main :badge :in 6))]
|
||||
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (= (:playback (get-in doc [:symbols :main :nodes :badge]))
|
||||
(:playback (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (= (get-in before [6 [:badge :mark]])
|
||||
(get-in (sample after [6]) [6 [:badge :mark]]))
|
||||
"the same animation on the frames it kept")
|
||||
(is (nil? (get-in (sample after [5]) [5 [:badge :mark]]))
|
||||
"and the frames it gave up show nothing of it")))
|
||||
|
||||
(deftest trim-refuses-to-lengthen-or-to-land-on-an-edge
|
||||
(let [doc (document)]
|
||||
(doseq [[label id edge to] [["at its own start" :b :in 4]
|
||||
["at its own end" :b :out 8]
|
||||
["past its end" :b :out 9]
|
||||
["before its start" :b :in 2]
|
||||
["off a whole frame" :b :out 5.5]
|
||||
["past the end of one outside a lane" :badge :out 11]
|
||||
["before the start of one outside a lane" :badge :in 1]]]
|
||||
(is (:refused (span/trim doc :main id edge to)) label))
|
||||
(is (:refused (span/trim doc :main :b :middle 6)))
|
||||
(is (re-find #"group" (:refused (span/trim doc :main :girl :out 6))))))
|
||||
|
||||
(deftest timeline-edge-resize-allows-an-ordinary-clip-to-grow
|
||||
(let [after (:clip (span/resize-out (document) :main :badge 11))]
|
||||
(deftest resizing-an-ordinary-placement-may-overlap
|
||||
(let [doc (ordinary-document)
|
||||
after (:clip (span/resize-out doc :main :badge 11 {}))]
|
||||
(is (= [2 11] (node/placed-span (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (:refused (span/resize-out (document) :main :badge 2)))))
|
||||
(is (:refused (span/resize-out doc :main :badge 2 {})))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; move
|
||||
(deftest moving-follows-the-symbol-mode
|
||||
(let [lane (document)
|
||||
ordinary (ordinary-document)]
|
||||
(is (:refused (span/move lane :main :insert 6)) "a lane clip cannot overlap B")
|
||||
(let [after (:clip (span/move ordinary :main :badge 0))]
|
||||
(is (= [0 8] (node/placed-span (get-in after [:symbols :main :nodes :badge])))
|
||||
"an ordinary symbol is free to composite over occupied frames")
|
||||
(is (empty? (clip/problems after))))))
|
||||
|
||||
(deftest moving-keeps-its-length-and-its-source-origin
|
||||
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
|
||||
r (span/move doc :main :insert 4)
|
||||
after (:clip r)]
|
||||
(is (= [[0 4] [4 8]] (spans after [:a :insert])))
|
||||
(is (= :insert (:selection r)))
|
||||
(is (= (:playback (get-in doc [:symbols :main :nodes :insert]))
|
||||
(:playback (get-in after [:symbols :main :nodes :insert]))))
|
||||
;; It began on source frame 3 at lane 8; it begins on source frame 3 at lane 4.
|
||||
(is (= (get-in (sample doc [8]) [8 [:insert :mark]])
|
||||
(get-in (sample after [4]) [4 [:insert :mark]])))
|
||||
(deftest clearing-room-then-moving-is-explicit-composition
|
||||
(let [doc (document)
|
||||
cleared (:clip (span/blank doc :main [4 8] {}))
|
||||
after (:clip (span/move cleared :main :insert 4))]
|
||||
(is (= [4 8] (node/placed-span (get-in after [:symbols :main :nodes :insert]))))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-move-outside-a-lane-is-free-to-land-on-an-occupied-frame
|
||||
;; The non-overlap rule is the LANE's, and `:badge` is not in one. Things
|
||||
;; placed in a composition are allowed to be on screen together, so there is
|
||||
;; nothing here for a move to refuse.
|
||||
(let [doc (document)
|
||||
r (span/move doc :main :badge 0)
|
||||
after (:clip r)]
|
||||
(is (= [0 8] (node/placed-span (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (= [[0 4] [4 8] [8 12]] (spans after [:a :b :insert]))
|
||||
"and the lane beside it did not notice")
|
||||
(is (= (get-in (sample doc [2]) [2 [:badge :mark]])
|
||||
(get-in (sample after [0]) [0 [:badge :mark]]))
|
||||
"its source origin came with it")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-move-onto-an-occupied-frame-of-a-lane-is-refused-rather-than-rippled
|
||||
(let [doc (document)]
|
||||
(is (:refused (span/move doc :main :insert 6)) "it would overlap B")
|
||||
(is (:refused (span/move doc :main :insert 4.5)))
|
||||
(is (re-find #"group" (:refused (span/move doc :main :girl 2))))
|
||||
(is (re-find #"whole shot" (:refused (span/move doc :main :plate 2))))
|
||||
;; Clearing the room first is the composition, and then it goes.
|
||||
(let [cleared (:clip (lane/blank doc :main :girl [4 8] {}))]
|
||||
(is (= [[0 4] [4 8]] (spans (:clip (span/move cleared :main :insert 4))
|
||||
[:a :insert]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the coordinate
|
||||
|
||||
(deftest host-frame-reads-lane-time-for-a-cel-and-symbol-time-for-everything-else
|
||||
(let [doc (document)
|
||||
retimed (assoc-in doc [:symbols :main :nodes :girl :time] {:at 4 :rate 2})]
|
||||
(is (= 6 (span/host-frame doc :main :b 6))
|
||||
"an untimed lane reads the symbol's frames as its own")
|
||||
(is (= 6 (span/host-frame doc :main :badge 6))
|
||||
"and so does a node with no parent, always")
|
||||
(is (= 4 (span/host-frame retimed :main :b 6))
|
||||
"through a lane at :at 4 :rate 2, symbol frame 6 is lane frame 4")
|
||||
(is (= 6 (span/host-frame retimed :main :badge 6))
|
||||
"which is the lane's business and not the badge's")
|
||||
(is (nil? (span/host-frame (assoc-in doc [:symbols :main :nodes :girl :time]
|
||||
{:loop? true})
|
||||
:main :b 6))
|
||||
"and a looping parent has no single answer to give")))
|
||||
(deftest host-frame-of-a-parentless-clip-is-the-symbol-frame
|
||||
(is (= 6 (span/host-frame (document) :main :b 6)))
|
||||
(is (= 6 (span/host-frame (ordinary-document) :main :badge 6))))
|
||||
|
|
|
|||
|
|
@ -1,28 +1,53 @@
|
|||
(ns arthur.events.lane-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.domain.lane-test :as fixture]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.correction :as correction]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[arthur.domain.sequence-test :as fixture]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.footage.store :as store]
|
||||
[arthur.ui.timeline :as timeline]))
|
||||
[arthur.ui.timeline :as timeline]
|
||||
[re-frame.core :as rf]
|
||||
[re-frame.db :as rf-db]))
|
||||
|
||||
(deftest one-row-projects-all-cels-and-keeps-selection-addresses
|
||||
(deftest symbol-and-lane-creation-are-distinct-explicit-commands
|
||||
(letfn [(run [event key]
|
||||
(let [doc (clip/blank)
|
||||
id (store/install! {:clip doc :store {}} key)]
|
||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main} :playback {:frame 0}})
|
||||
(rf/dispatch-sync event)
|
||||
(let [db @rf-db/app-db
|
||||
saved (:clip (store/entry id))
|
||||
[_ _ instance-id] (get-in db [:ui :selection])
|
||||
sid (get-in saved [:symbols :main :nodes instance-id :source :symbol])]
|
||||
{:db db :symbol (clip/symbol saved sid)})))]
|
||||
(let [{ordinary :symbol} (run [::ui/new-symbol :inside] "explicit-symbol")
|
||||
{lane :symbol lane-db :db} (run [::ui/new-lane] "explicit-lane")]
|
||||
(is (nil? (:display ordinary)) "new symbol means ordinary symbol")
|
||||
(is (= :lane (:display lane)) "only the lane command creates a lane")
|
||||
(is (some? (get-in lane-db [:ui :target]))
|
||||
"the new lane is aimed so drawing and pool drops can go into it"))))
|
||||
|
||||
(deftest an-explicit-lane-is-one-row-of-clips
|
||||
(let [doc (fixture/document)
|
||||
rows (timeline/rows doc :main #{})
|
||||
lane (first (filter :cels rows))]
|
||||
(is (= 2 (count rows)))
|
||||
lane (first (filter :lane? rows))]
|
||||
(is (= 2 (count rows)) "the lane row plus the span-less plate")
|
||||
(is (= [[0 4] [4 8] [8 12]] (mapv :span (:cels lane))))
|
||||
(is (= [[:node :main :a [:a]] [:node :main :b [:b]] [:node :main :insert [:insert]]]
|
||||
(mapv :select (:cels lane))))
|
||||
(is (= [0 6 12] (:keys lane)))
|
||||
(is (= 1 (count (filter :cels (timeline/rows doc :main #{[:girl]})))))))
|
||||
(is (= [[:node :main :a [:a]]
|
||||
[:node :main :b [:b]]
|
||||
[:node :main :insert [:insert]]]
|
||||
(mapv :select (:cels lane))))))
|
||||
|
||||
(deftest a-nested-selection-converts-the-open-playhead-to-its-owning-symbol
|
||||
(deftest an-ordinary-symbol-keeps-a-row-per-node
|
||||
(let [doc (update-in (fixture/document) [:symbols :main] dissoc :display)
|
||||
rows (timeline/rows doc :main #{})]
|
||||
(is (empty? (filter :lane? rows)))
|
||||
(is (= #{[:a] [:b] [:insert] [:plate]} (set (map :path rows))))))
|
||||
|
||||
(deftest a-nested-selection-converts-the-open-playhead-to-its-owner
|
||||
(let [doc (assoc-in (fixture/document) [:symbols :outer]
|
||||
{:id :outer :frames 30
|
||||
:nodes {:take {:id :take :kind :instance :z "a"
|
||||
|
|
@ -34,189 +59,59 @@
|
|||
(is (= 12 (ui/selection-frame doc nil :main
|
||||
[:node :main :a [:a]] 12)))))
|
||||
|
||||
(deftest polygon-landing-follows-the-target-not-the-selection
|
||||
(let [doc (fixture/document)
|
||||
db {:ui {:open :main
|
||||
:selection [:node :main :plate [:plate]]
|
||||
:target {:sid :main :id :insert :path [:insert]}}
|
||||
:playback {:frame 5}}
|
||||
landing (ui/polygon-landing doc {} db)]
|
||||
(is (= doc (:clip landing)))
|
||||
(is (= [:insert] (:path landing))
|
||||
"looking at another shape does not silently move the creation target")
|
||||
(is (false? (:lane? landing)))))
|
||||
(deftest polygon-landing-obeys-the-destination-symbol-mode
|
||||
(let [lane (fixture/document)
|
||||
ordinary (update-in lane [:symbols :main] dissoc :display)
|
||||
db {:ui {:open :main :target {:sid :main :id :plate :path [:plate]}}
|
||||
:playback {:frame 5}}]
|
||||
(is (true? (:lane? (ui/polygon-landing lane {} db))))
|
||||
(is (false? (:lane? (ui/polygon-landing ordinary {} db))))))
|
||||
|
||||
(deftest timeline-polygon-landing-is-decided-by-the-aimed-lane
|
||||
(let [doc (fixture/document)
|
||||
base {:ui {:open :main
|
||||
:selection [:node :main :plate [:plate]]
|
||||
:target {:sid :main :id :girl :path [:girl]}}
|
||||
:playback {:frame 5}}
|
||||
occupied (ui/polygon-landing doc {} base)
|
||||
with-gap (:clip (lane/blank doc :main :girl [5 7] {:id :rest}))
|
||||
gap (ui/polygon-landing with-gap {} (assoc-in base [:playback :frame] 6))]
|
||||
(is (= [:b] (:path occupied))
|
||||
"the cel on screen wins even while an unrelated stage node is selected")
|
||||
(is (= doc (:clip occupied)) "an existing cel needs no document edit")
|
||||
(is (true? (:lane? occupied)))
|
||||
(is (= 1 (count (:path gap))))
|
||||
(is (not (contains? (get-in with-gap [:symbols :main :nodes])
|
||||
(first (:path gap))))
|
||||
"a gap receives a fresh drawing")
|
||||
(is (contains? (get-in (:clip gap) [:symbols :main :nodes])
|
||||
(first (:path gap))))
|
||||
(is (empty? (clip/problems (:clip gap))))))
|
||||
|
||||
(deftest polygon-landing-without-an-aimed-lane-uses-the-ordinary-target
|
||||
(let [doc (fixture/document)
|
||||
db {:ui {:open :main
|
||||
:target {:sid :main :id :plate :path [:plate]}}
|
||||
:playback {:frame 5}}
|
||||
landing (ui/polygon-landing doc {} db)]
|
||||
(is (= doc (:clip landing)))
|
||||
(is (= [] (:path landing)))
|
||||
(is (false? (:lane? landing)))))
|
||||
|
||||
(deftest beginning-a-polygon-materializes-a-missing-timeline-drawing
|
||||
(let [doc (fixture/document)
|
||||
with-gap (:clip (lane/blank doc :main :girl [5 7] {:id :rest}))
|
||||
id (store/install! {:clip with-gap :store {}} "polygon-start-test")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main
|
||||
:target {:sid :main :id :girl :path [:girl]}}
|
||||
:playback {:frame 6}}
|
||||
after (ui/beginning-polygon db)
|
||||
saved (:clip (store/entry (:clip/current after)))
|
||||
[_ sid cel-id path] (get-in after [:ui :selection])]
|
||||
(is (= :polygon (get-in after [:ui :tool])))
|
||||
(is (= [] (get-in after [:ui :draft])))
|
||||
(is (= :main sid))
|
||||
(is (= [cel-id] path))
|
||||
(is (contains? (get-in saved [:symbols :main :nodes]) cel-id)
|
||||
"the drawing exists before the first draft point is added")
|
||||
(is (= 6 (get-in saved [:symbols :main :nodes cel-id :time :at])))
|
||||
(is (empty? (clip/problems saved)))))
|
||||
|
||||
(deftest beginning-a-polygon-does-not-invent-an-unselected-lane
|
||||
(deftest beginning-a-polygon-does-not-invent-a-lane
|
||||
(let [doc (clip/blank)
|
||||
id (store/install! {:clip doc :store {}} "polygon-lane-start-test")
|
||||
id (store/install! {:clip doc :store {}} "polygon-no-implicit-lane")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main}
|
||||
:playback {:frame 6}}
|
||||
after (ui/beginning-polygon db)
|
||||
saved (:clip (store/entry (:clip/current after)))
|
||||
lanes (symbol/lanes (get-in saved [:symbols :main :nodes]))]
|
||||
saved (:clip (store/entry (:clip/current after)))]
|
||||
(is (= :polygon (get-in after [:ui :tool])))
|
||||
(is (= [:lane] (mapv :id lanes))
|
||||
"the lane the symbol was born with, and no second one invented here")
|
||||
(is (empty? (symbol/lane-clips (get-in saved [:symbols :main :nodes]) :lane))
|
||||
"and nothing put in it")
|
||||
(is (nil? (get-in after [:ui :target])))
|
||||
(is (nil? (get-in (store/entry (:clip/current after)) [:history :done])))
|
||||
(is (empty? (clip/problems saved)))))
|
||||
(is (= {} (get-in saved [:symbols :main :nodes])))
|
||||
(is (nil? (get-in saved [:symbols :main :display])))
|
||||
(is (nil? (get-in (store/entry (:clip/current after)) [:history :done])))))
|
||||
|
||||
(deftest sequence-commands-use-isolated-history-transactions
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "sequence-test")
|
||||
id (store/install! {:clip doc :store {}} "sequence-command-test")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :selection [:node :main :a [:a]]}}
|
||||
refused (ui/apply-lane-command db :main
|
||||
(lane/extend-hold doc :main :a 1 {}) [:retry])]
|
||||
refused (ui/apply-command db :main
|
||||
(span/extend-hold doc :main :a 1 {}) [:retry])]
|
||||
(is (= doc (:clip (store/entry id))))
|
||||
(is (nil? (:history (store/entry id))))
|
||||
(is (= [:retry] (get-in refused [:ui :lane-retry])))
|
||||
(let [r1 (lane/extend-hold doc :main :a 1 {:extent :grow-symbol})
|
||||
db1 (ui/apply-lane-command db :main r1 nil)
|
||||
r2 (lane/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
|
||||
db2 (ui/apply-lane-command db1 :main r2 nil)
|
||||
(is (= [:retry] (get-in refused [:ui :retry])))
|
||||
(let [r1 (span/extend-hold doc :main :a 1 {:extent :grow-symbol})
|
||||
db1 (ui/apply-command db :main r1 nil)
|
||||
r2 (span/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
|
||||
db2 (ui/apply-command db1 :main r2 nil)
|
||||
h (:history (store/entry id))
|
||||
undo (history/undo h (leaf/leaves "u" (:clip r2)))
|
||||
undo2 (history/undo (:history undo) (:leaves undo))]
|
||||
(is (= 2 (count (:done h))) "rapid button presses remain separate commands")
|
||||
(is (= 2 (count (:done h))))
|
||||
(is (= (:clip r1) (leaf/clip "u" (:leaves undo))))
|
||||
(is (= doc (leaf/clip "u" (:leaves undo2))))
|
||||
(is (= [:node :main :a [:a]] (get-in db2 [:ui :selection]))))))
|
||||
|
||||
(deftest a-correction-is-one-step-and-keeps-the-full-selection-address
|
||||
(deftest an-expanded-lane-opens-the-selected-clip
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "correction-event-test")
|
||||
selection [:node :main :a [:outer :a]]
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :outer :selection selection}}
|
||||
result (correction/add doc :main :a [:xform :rot]
|
||||
{:id :nudge :support [0 3] :motion :return
|
||||
:start 0 :peak 0.5 :peak-frame 1})
|
||||
after (ui/apply-correction-command db result)
|
||||
entry (store/entry id)]
|
||||
(is (= selection (get-in after [:ui :selection])))
|
||||
(is (= 1 (count (get-in entry [:history :done]))))
|
||||
(is (= 0.5 (get-in (:clip entry)
|
||||
[:symbols :main :nodes :a :channels [:xform :rot]
|
||||
:over 0 :values :keys 1])))
|
||||
(let [refused (ui/apply-correction-command after {:refused "nope"})]
|
||||
(is (= "nope" (get-in refused [:project :status])))
|
||||
(is (= 1 (count (get-in (store/entry id) [:history :done])))))))
|
||||
rows (timeline/rows doc :main #{[:arthur.ui.timeline/lane] [:insert]} [:insert])
|
||||
portal (first (filter :portal? rows))]
|
||||
(is (= [:insert] (:path portal)))
|
||||
(is (= [:node :main :insert [:insert]] (:select portal)))
|
||||
(is (some #{"mark"} (map :label rows)))))
|
||||
|
||||
(deftest an-expanded-lane-opens-the-selected-clip-and-everything-under-it
|
||||
;; The whole document is editable from the root timeline: a lane opens one
|
||||
;; portal — the clip selected in it — and that portal opens the lanes and
|
||||
;; nodes of the symbol it places, mapped into this ruler.
|
||||
(let [doc (fixture/document)
|
||||
open #{[:girl] [:insert]}
|
||||
shut (timeline/rows doc :main open [:insert])
|
||||
of (fn [rows] (mapv (juxt :label :depth) rows))
|
||||
lane-row (first (filter :cels (timeline/rows doc :main open [:insert])))]
|
||||
(is (= 1 (count (filter :portal? shut)))
|
||||
"exactly one clip is opened, not one branch per clip in the lane")
|
||||
(is (= [:insert] (:path (first (filter :portal? shut))))
|
||||
"and it is the selected one")
|
||||
(is (some #{["mark" 2]} (of shut))
|
||||
"the clip's own symbol appears under it, at its depth")
|
||||
(is (= [:node :main :insert [:insert]]
|
||||
(:select (first (filter :portal? shut))))
|
||||
"the portal addresses the same clip its block in the lane does")
|
||||
(is (= (:keys (second (:cels lane-row)))
|
||||
(:keys (first (filter #(= [:b] (:path %)) (timeline/rows doc :main open [:b])))))
|
||||
"a clip's keys are on its block whether or not its portal is open")))
|
||||
|
||||
(deftest a-nested-selection-keeps-the-portal-that-revealed-it-open
|
||||
;; Clicking a shape inside the clip — or the end of its span — is still
|
||||
;; working inside that clip. Matching the selected id alone would close the
|
||||
;; portal the moment anything under it was touched.
|
||||
(let [doc (fixture/document)
|
||||
open #{[:girl] [:insert]}
|
||||
deep (timeline/rows doc :main open [:insert :mark])
|
||||
none (timeline/rows doc :main open [:plate])]
|
||||
(is (= [:insert] (:path (first (filter :portal? deep))))
|
||||
"a selection under the clip keeps that clip's portal")
|
||||
(is (empty? (filter :portal? none)))
|
||||
(is (some #{"select a clip to inspect"} (map :label none))
|
||||
"with nothing selected in it, an open lane says what it is waiting for")))
|
||||
|
||||
(deftest a-held-clip-shows-its-contents-without-inventing-frames-for-them
|
||||
;; `clip/source-time` is nil for a hold, so nested keys have no place on this
|
||||
;; ruler — but the drawing's own nodes must still be reachable from here.
|
||||
(let [doc (fixture/document)
|
||||
rows (timeline/rows doc :main #{[:girl] [:a]} [:a])
|
||||
inside (filter :unmapped? rows)]
|
||||
(is (seq inside) "a held drawing opens")
|
||||
(is (some #{"mark"} (map :label inside)))
|
||||
(is (every? (comp empty? :keys) inside)
|
||||
"no key is placed where the hold cannot say it belongs")
|
||||
(is (= [[0 4]] (distinct (keep :span (filter #(= :node (:kind %)) inside))))
|
||||
"its rows span the hold, which is when it is on screen")))
|
||||
|
||||
(deftest a-lane-of-sounds-is-drawn-as-a-lane-and-not-flattened-twice
|
||||
(let [made (lane/add-lane (fixture/document) :main :track)
|
||||
seeded (clip/place-sound (:clip made) :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
doc (:clip (lane/adopt seeded :main :track :vo 2 {:extent :grow-symbol}))
|
||||
picture (remove :sound? (timeline/rows doc :main #{} nil))
|
||||
sound-lanes (filter :sound? (timeline/rows doc :main #{} nil))
|
||||
flattened (timeline/sound-rows doc :main #{})]
|
||||
(is (= 1 (count sound-lanes)) "the sound's lane is one row, like any lane")
|
||||
(is (= [:vo] (mapv :id (:cels (first sound-lanes))))
|
||||
"with the sound on it as a block that can be moved and trimmed")
|
||||
(is (empty? (filter #(= [:track] (:path %)) picture))
|
||||
"and it is not also listed among the picture rows")
|
||||
(is (empty? flattened)
|
||||
"nor flattened into a second, parallel audio row")))
|
||||
(deftest a-lane-of-sounds-is-not-flattened-twice
|
||||
(let [base (:clip (span/draw-as-lane (clip/blank) :main true {}))
|
||||
doc (clip/place-sound base :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
lane (first (filter :lane? (timeline/rows doc :main #{} nil)))]
|
||||
(is (= [:vo] (mapv :id (:cels lane))))
|
||||
(is (empty? (timeline/sound-rows doc :main #{})))))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Local editor smoke test. Uses the in-memory blank document and disables the
|
||||
// project route, so it never creates an account, project, or server-side write.
|
||||
// Browser smoke test for the explicit-lane workflow. It uses the in-memory
|
||||
// document and disables project routing, so it performs no server-side write.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
|
@ -7,7 +7,7 @@ import { join } from 'node:path';
|
|||
import assert from 'node:assert/strict';
|
||||
|
||||
const url = process.env.ARTHUR_URL ?? 'http://localhost:8778/';
|
||||
const profile = mkdtempSync(join(tmpdir(), 'arthur-sequence-'));
|
||||
const profile = mkdtempSync(join(tmpdir(), 'arthur-explicit-lane-'));
|
||||
const port = 9335;
|
||||
const chrome = spawn(process.env.CHROME ?? '/usr/bin/chromium', [
|
||||
'--headless=new', '--no-sandbox', '--disable-gpu', '--no-first-run',
|
||||
|
|
@ -16,6 +16,7 @@ const chrome = spawn(process.env.CHROME ?? '/usr/bin/chromium', [
|
|||
], { stdio: 'ignore' });
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let ws;
|
||||
|
||||
try {
|
||||
let target;
|
||||
for (let i = 0; i < 100 && !target; i++) {
|
||||
|
|
@ -23,11 +24,12 @@ try {
|
|||
try {
|
||||
target = (await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json()))
|
||||
.find(t => t.type === 'page' && t.url.startsWith(url));
|
||||
} catch { /* browser starting */ }
|
||||
} catch { /* Chromium is still starting. */ }
|
||||
}
|
||||
assert(target, 'browser exposes the editor page');
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
|
||||
let serial = 0;
|
||||
const pending = new Map();
|
||||
const errors = [];
|
||||
|
|
@ -35,10 +37,10 @@ try {
|
|||
const msg = JSON.parse(data);
|
||||
if (msg.method === 'Runtime.exceptionThrown') errors.push(msg.params.exceptionDetails);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const { resolve, reject } = pending.get(msg.id);
|
||||
const waiting = pending.get(msg.id);
|
||||
pending.delete(msg.id);
|
||||
if (msg.error) reject(new Error(JSON.stringify(msg.error)));
|
||||
else resolve(msg.result);
|
||||
if (msg.error) waiting.reject(new Error(JSON.stringify(msg.error)));
|
||||
else waiting.resolve(msg.result);
|
||||
}
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
|
|
@ -47,13 +49,16 @@ try {
|
|||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const evaluate = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails));
|
||||
return r.result.value;
|
||||
const result = await send('Runtime.evaluate', {
|
||||
expression, returnByValue: true, awaitPromise: true,
|
||||
});
|
||||
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
|
||||
return result.result.value;
|
||||
};
|
||||
|
||||
await send('Runtime.enable');
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (await evaluate('typeof arthur !== "undefined" && !!arthur.events?.ui && !!document.querySelector("canvas.stage")')) break;
|
||||
if (await evaluate('typeof arthur !== "undefined" && !!document.querySelector("canvas.stage")')) break;
|
||||
await sleep(100);
|
||||
}
|
||||
await evaluate(`(() => {
|
||||
|
|
@ -61,342 +66,145 @@ try {
|
|||
cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db, k('route'), k('local-test')));
|
||||
window.laneSnapshot = () => {
|
||||
const db = cljs.core.deref(re_frame.db.app_db);
|
||||
const entry = arthur.footage.store.entry(cljs.core.get(db, k('clip/current')));
|
||||
return cljs.core.clj__GT_js(entry);
|
||||
return cljs.core.clj__GT_js(arthur.footage.store.entry(cljs.core.get(db, k('clip/current'))));
|
||||
};
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
// A command is named the same wherever it is drawn, and since the transport
|
||||
// strip was consolidated it is drawn in one of two places: as a button in the
|
||||
// strip, or as a row in one of the strip's menus. So the test asks for it by
|
||||
// name and this finds it — opening each menu in turn to look — rather than the
|
||||
// test knowing which menu anything ended up in. An icon button is matched on
|
||||
// its `aria-label`, which is also what a screen reader is told it is.
|
||||
// Two bars carry commands: the location bar says where an edit lands and holds
|
||||
// what creates things there, the transport strip holds what acts on a cel.
|
||||
const bars = ['.loc', '.pane.time .pane-head', '.section'];
|
||||
const within = (suffix) => bars.map((b) => `${b} ${suffix}`).join(', ');
|
||||
const named = label =>
|
||||
`(b => b.textContent.trim() === ${JSON.stringify(label)}` +
|
||||
` || b.getAttribute('aria-label') === ${JSON.stringify(label)})`;
|
||||
const shut = async () => {
|
||||
await evaluate(`(() => { document.querySelectorAll('.menu-scrim').forEach(s => s.click()); return true })()`);
|
||||
await sleep(120);
|
||||
|
||||
const named = label => `(b => b.getAttribute('aria-label') === ${JSON.stringify(label)}` +
|
||||
` || b.textContent.trim() === ${JSON.stringify(label)})`;
|
||||
const closeMenus = async () => {
|
||||
await evaluate(`(() => { document.querySelectorAll('.menu-scrim').forEach(x => x.click()); return true })()`);
|
||||
await sleep(80);
|
||||
};
|
||||
// Leaves the control on screen and returns what to select it with.
|
||||
const reveal = async label => {
|
||||
await shut();
|
||||
if (await evaluate(`![...document.querySelectorAll('${within('button')}')].find(${named(label)})`)) {
|
||||
const menus = await evaluate(
|
||||
`[...document.querySelectorAll('${within('.menu-wrap > button')}')].map(b => b.textContent.trim())`);
|
||||
let found = false;
|
||||
for (const menu of menus) {
|
||||
await evaluate(`(() => { [...document.querySelectorAll('${within('.menu-wrap > button')}')]
|
||||
.find(b => b.textContent.trim() === ${JSON.stringify(menu)}).click(); return true })()`);
|
||||
await sleep(180);
|
||||
if (await evaluate(`!![...document.querySelectorAll('.menu-item')].find(${named(label)})`)) { found = true; break; }
|
||||
await shut();
|
||||
}
|
||||
assert(found, `a control named: ${label}`);
|
||||
return '.menu-item';
|
||||
}
|
||||
return within('button');
|
||||
};
|
||||
const click = async label => {
|
||||
const where = await reveal(label);
|
||||
const clickNew = async label => {
|
||||
await closeMenus();
|
||||
assert(await evaluate(`(() => {
|
||||
const b = [...document.querySelectorAll('${where}')].find(${named(label)});
|
||||
if (!b || b.disabled) return false;
|
||||
b.click(); return true;
|
||||
})()`), `enabled control: ${label}`);
|
||||
await sleep(180);
|
||||
await shut();
|
||||
const menu = document.querySelector('.loc .menu-wrap > button');
|
||||
if (!menu) return false;
|
||||
menu.click(); return true;
|
||||
})()`), 'new menu exists');
|
||||
await sleep(100);
|
||||
assert(await evaluate(`(() => {
|
||||
const item = [...document.querySelectorAll('.menu-item')].find(${named(label)});
|
||||
if (!item || item.disabled) return false;
|
||||
item.click(); return true;
|
||||
})()`), `enabled creation command: ${label}`);
|
||||
await sleep(220);
|
||||
await closeMenus();
|
||||
};
|
||||
// UUIDs expose a mutable hash cache through clj->js; compare their identity,
|
||||
// not that implementation detail, when asserting exact undo restoration.
|
||||
const shot = async () => JSON.parse(JSON.stringify(await evaluate('laneSnapshot()'),
|
||||
(_key, value) => value?.uuid ?? value));
|
||||
const instances = s => Object.values(s.clip.symbols.main.nodes)
|
||||
.filter(n => n.kind === 'instance')
|
||||
.sort((a, b) => a.time.at - b.time.at);
|
||||
const placed = s => instances(s).map(n => [
|
||||
n.time.at + n.span[0] / (n.time.rate ?? 1),
|
||||
n.time.at + n.span[1] / (n.time.rate ?? 1),
|
||||
]);
|
||||
const key = async (key, extra = {}) => {
|
||||
await send('Input.dispatchKeyEvent', {type: 'keyDown', key, ...extra});
|
||||
await send('Input.dispatchKeyEvent', {type: 'keyUp', key, ...extra});
|
||||
await sleep(180);
|
||||
};
|
||||
const undo = () => key('z', {modifiers: 2});
|
||||
const drag = async (selector, df, {zone = 0.5, shift = false} = {}) => {
|
||||
const points = await evaluate(`(() => {
|
||||
const handle = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!handle) return null;
|
||||
const track = handle.closest('.tl-track');
|
||||
const h = handle.getBoundingClientRect();
|
||||
const t = track.getBoundingClientRect();
|
||||
const frames = Number(document.querySelector('.at-frame').textContent.split('/')[1]);
|
||||
const x = h.left + h.width * ${zone};
|
||||
const y = h.top + h.height / 2;
|
||||
return {x, y, end: x + t.width * ${df} / frames};
|
||||
})()`);
|
||||
assert(points, `drag handle exists: ${selector}`);
|
||||
const modifiers = shift ? 8 : 0;
|
||||
await send('Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed', x: points.x, y: points.y,
|
||||
button: 'left', buttons: 1, clickCount: 1, modifiers,
|
||||
});
|
||||
await sleep(100);
|
||||
await send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseMoved', x: points.end, y: points.y,
|
||||
button: 'left', buttons: 1, modifiers,
|
||||
});
|
||||
await sleep(100);
|
||||
await send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseReleased', x: points.end, y: points.y,
|
||||
button: 'left', buttons: 0, clickCount: 1, modifiers,
|
||||
});
|
||||
await sleep(250);
|
||||
};
|
||||
const tabs = () => evaluate(`(() => {
|
||||
const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db);
|
||||
return {tabs: cljs.core.clj__GT_js(cljs.core.get_in(db, [k('ui'), k('tabs')])).map(String),
|
||||
open: String(cljs.core.clj__GT_js(cljs.core.get_in(db, [k('ui'), k('open')])))};
|
||||
})()`);
|
||||
// A real two-press double-click, not `.dispatchEvent`: what broke here was
|
||||
// where the browser decides to deliver the click, which a synthetic event
|
||||
// cannot show.
|
||||
const doubleClick = async selector => {
|
||||
const p = await evaluate(`(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
return {x: r.left + r.width / 2, y: r.top + r.height / 2};
|
||||
})()`);
|
||||
assert(p, `something to double-click: ${selector}`);
|
||||
for (const clickCount of [1, 2]) {
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: p.x, y: p.y,
|
||||
button: 'left', buttons: 1, clickCount});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: p.x, y: p.y,
|
||||
button: 'left', buttons: 0, clickCount});
|
||||
await sleep(60);
|
||||
}
|
||||
await sleep(280);
|
||||
};
|
||||
const dropPoolSymbol = async frame => {
|
||||
const points = await evaluate(`(() => {
|
||||
const source = document.querySelector('.pool-row:not(.main) .pool-item[draggable="true"]');
|
||||
const track = document.querySelector('.tl-track');
|
||||
if (!source || !track) return null;
|
||||
source.scrollIntoView({block: 'center'});
|
||||
const a = source.getBoundingClientRect(), b = track.getBoundingClientRect();
|
||||
const frames = Number(document.querySelector('.at-frame').textContent.split('/')[1]);
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * (${frame} + 0.25) / frames,
|
||||
ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
assert(points, 'a library symbol and lane are available to drag');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.sx, y: points.sy});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: points.sx, y: points.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.sx + 12, y: points.sy,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(120);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(120);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.ghost").length'), 0,
|
||||
'targeting an existing lane does not preview a temporary new row');
|
||||
const lanePreview = await evaluate(`(() => {
|
||||
const db = cljs.core.deref(re_frame.db.app_db), k = cljs.core.keyword;
|
||||
return {ghosts: document.querySelectorAll('.tl-track .tl-cel.ghost').length,
|
||||
drop: cljs.core.clj__GT_js(cljs.core.get_in(db, [k('ui'), k('drop')]))};
|
||||
})()`);
|
||||
assert.equal(lanePreview.ghosts, 1,
|
||||
`the pool drop preview is drawn inside the targeted lane: ${JSON.stringify(lanePreview)}`);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
};
|
||||
const dragClipBetweenLanes = async frame => {
|
||||
const points = await evaluate(`(() => {
|
||||
const tracks = [...document.querySelectorAll('.tl-track')];
|
||||
const source = tracks[1]?.querySelector('.tl-cel');
|
||||
const target = tracks[0];
|
||||
if (!source || !target) return null;
|
||||
const a = source.getBoundingClientRect(), b = target.getBoundingClientRect();
|
||||
const frames = Number(document.querySelector('.at-frame').textContent.split('/')[1]);
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * (${frame} + 0.25) / frames,
|
||||
ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
assert(points, 'two lanes and a source clip are available');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: points.sx, y: points.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(150);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-track")[0].querySelectorAll(".tl-cel.ghost").length'), 1,
|
||||
'cross-lane movement previews in the destination lane');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
};
|
||||
const mainInstances = s => Object.values(s.clip.symbols.main.nodes)
|
||||
.filter(n => n.kind === 'instance');
|
||||
const laneSymbols = s => Object.values(s.clip.symbols).filter(sym => sym.display === 'lane');
|
||||
|
||||
assert.equal(await evaluate('[...document.querySelectorAll(".timing-controls > button")].every(b => b.disabled)'), true,
|
||||
'timing buttons are disabled without a symbol clip');
|
||||
await click('inside');
|
||||
assert.equal((await shot()).clip.symbols.main.display, undefined,
|
||||
'a blank document starts as an ordinary symbol');
|
||||
|
||||
await clickNew('inside');
|
||||
let s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 1]],
|
||||
'new at the root automatically makes a lane and a one-frame symbol clip');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label .kind").length'), 1,
|
||||
'new temporal content creates a lane row rather than a row per symbol');
|
||||
assert.equal(await evaluate(`document.querySelectorAll('.cel-sheet, [aria-label="time view"]').length`), 0,
|
||||
'there is one temporal interface');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".timing-controls > button").length'), 3,
|
||||
'timing operations are direct buttons');
|
||||
let placed = mainInstances(s);
|
||||
assert.equal(placed.length, 1, 'new symbol places one instance');
|
||||
assert.equal(s.clip.symbols[placed[0].source.symbol].display, undefined,
|
||||
'new symbol remains ordinary');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-track").length'), 1,
|
||||
'an ordinary symbol is an ordinary timeline row');
|
||||
|
||||
await drag('.tl-cel .tl-edge.out', 3);
|
||||
await clickNew('lane');
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 4]], 'a right edge directly changes the endpoint');
|
||||
placed = mainInstances(s);
|
||||
assert.equal(placed.length, 2, 'explicit lane places a second symbol');
|
||||
assert.equal(laneSymbols(s).length, 1, 'only the lane command marks a symbol as a lane');
|
||||
assert.equal(await evaluate('[...document.querySelectorAll(".tl-track")].filter(t => t.arthurLane).length'), 1,
|
||||
'the explicit lane is drawn as one linear track');
|
||||
|
||||
for (let i = 0; i < 4; i++) await click('+1');
|
||||
await click('inside');
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-edge.out', 2);
|
||||
for (let i = 0; i < 3; i++) await click('+1');
|
||||
await click('inside');
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 4], [4, 7], [7, 8]]);
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-junction', 1, {zone: 0.5});
|
||||
assert.deepEqual(placed(await shot()), [[0, 5], [5, 7], [7, 8]],
|
||||
'the middle of a junction rolls both edges');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-junction', 1, {zone: 0.9});
|
||||
assert.deepEqual(placed(await shot()), [[0, 4], [5, 7], [7, 8]],
|
||||
'the right side trims only the right clip');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-junction', -1, {zone: 0.1});
|
||||
assert.deepEqual(placed(await shot()), [[0, 3], [4, 7], [7, 8]],
|
||||
'the left side trims only the left clip');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-edge.out', 2, {shift: true});
|
||||
assert.deepEqual(placed(await shot()), [[0, 4], [4, 9], [9, 10]],
|
||||
'Shift-edge ripples every later clip on the lane');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:first-of-type .tl-edge.out', 2);
|
||||
assert.deepEqual(placed(await shot()), [[0, 6], [6, 7], [7, 8]],
|
||||
'ordinary growth trims adjacent spans and never overlaps');
|
||||
|
||||
await dropPoolSymbol(10);
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 6], [6, 7], [7, 8], [10, 11]],
|
||||
'an arbitrary library symbol drops into an existing lane');
|
||||
assert.equal(instances(s).at(-1).playback.speed, 1,
|
||||
'a dropped symbol plays naturally instead of becoming a held drawing');
|
||||
|
||||
await evaluate(`re_frame.core.dispatch(cljs.core.vector(cljs.core.keyword('arthur.events.ui/new-lane')))`);
|
||||
await sleep(180);
|
||||
s = await shot();
|
||||
const renameControls = await evaluate('document.querySelectorAll(".tl-label .tl-rename").length');
|
||||
assert.equal(renameControls, 2,
|
||||
`both lanes expose rename controls: ${JSON.stringify(s.clip.symbols.main.nodes)}`);
|
||||
await evaluate('document.querySelector(".tl-label .tl-rename").click()');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-rename").length'), 1,
|
||||
'the lane exposes its rename control');
|
||||
await evaluate('document.querySelector(".tl-rename").click()');
|
||||
await sleep(80);
|
||||
assert(await evaluate(`(() => {
|
||||
const input = document.querySelector('.tl-name-input');
|
||||
if (!input) return false;
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, 'Foreground');
|
||||
input.dispatchEvent(new InputEvent('input', {bubbles: true, inputType: 'insertText', data: 'Foreground'}));
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
|
||||
.set.call(input, 'Foreground');
|
||||
input.dispatchEvent(new InputEvent('input', {bubbles: true, inputType: 'insertText'}));
|
||||
input.blur(); return true;
|
||||
})()`), 'lane rename editor opens');
|
||||
await sleep(180);
|
||||
s = await shot();
|
||||
assert(Object.values(s.clip.symbols.main.nodes).some(n => n.layout === 'sequence' && n.name === 'Foreground'),
|
||||
'a lane name is editable and persisted in the document');
|
||||
assert(mainInstances(s).some(n => n.name === 'Foreground'), 'lane name persists');
|
||||
|
||||
await dragClipBetweenLanes(12);
|
||||
s = await shot();
|
||||
const lanes = Object.values(s.clip.symbols.main.nodes).filter(n => n.layout === 'sequence');
|
||||
assert.deepEqual(lanes.map(l => instances(s).filter(n => n.parent === l.id).length).sort(), [1, 3],
|
||||
'a clip body can move from one lane to another');
|
||||
|
||||
// EXPANDING A LANE OPENS THE SELECTED CLIP. Its own keys, and under it the
|
||||
// lanes and nodes of the symbol it places, all on this ruler — which is what
|
||||
// makes the whole document editable from the root timeline.
|
||||
const rowLabels = () => evaluate(
|
||||
`[...document.querySelectorAll('.tl-labels > .tl-label')].map(e => e.textContent.trim())`);
|
||||
const twist = async i => {
|
||||
assert(await evaluate(`(() => {
|
||||
const t = document.querySelectorAll('.tl-labels > .tl-label .tl-twist')[${i}];
|
||||
if (!t || t.disabled) return false;
|
||||
t.click(); return true;
|
||||
})()`), `an expander at row ${i}`);
|
||||
await sleep(220);
|
||||
};
|
||||
await evaluate(`(() => { document.querySelector('.tl-track .tl-cel').click(); return true })()`);
|
||||
await sleep(200);
|
||||
const collapsed = await rowLabels();
|
||||
await twist(0);
|
||||
const opened = await rowLabels();
|
||||
assert(opened.length > collapsed.length, 'the lane opens');
|
||||
assert.equal(opened.filter(l => l.includes('instance')).length, 1,
|
||||
`one clip portal, not one branch per clip: ${JSON.stringify(opened)}`);
|
||||
const portalAt = opened.findIndex(l => l.includes('instance'));
|
||||
await twist(portalAt);
|
||||
const deep = await rowLabels();
|
||||
assert(deep.length > opened.length,
|
||||
`the portal opens the symbol the clip places: ${JSON.stringify(deep)}`);
|
||||
// Selecting something nested must not close the portal that revealed it.
|
||||
await evaluate(`(() => {
|
||||
const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db);
|
||||
const sel = cljs.core.get_in(db, [k('ui'), k('selection')]);
|
||||
const path = cljs.core.nth(sel, 3);
|
||||
re_frame.core.dispatch(cljs.core.vector(
|
||||
k('arthur.events.ui/select'),
|
||||
cljs.core.vector(k('node'), cljs.core.nth(sel, 1), cljs.core.nth(sel, 2),
|
||||
cljs.core.conj(path, k('made-up-child')))));
|
||||
return true;
|
||||
// Drag a library symbol into the explicit lane. The row itself is the target;
|
||||
// no temporary lane is previewed or created.
|
||||
const drop = await evaluate(`(() => {
|
||||
const source = document.querySelector('.pool-row:not(.main) .pool-item[draggable="true"]');
|
||||
const track = [...document.querySelectorAll('.tl-track')].find(t => t.arthurLane);
|
||||
if (!source || !track) return null;
|
||||
source.scrollIntoView({block: 'center'});
|
||||
const a = source.getBoundingClientRect(), b = track.getBoundingClientRect();
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * .085, ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
await sleep(220);
|
||||
assert.equal((await rowLabels()).filter(l => l.includes('instance')).length, 1,
|
||||
'a selection under the clip keeps its portal open');
|
||||
await twist(portalAt);
|
||||
await twist(0);
|
||||
assert(drop, 'a pool symbol and explicit lane are available');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx, y: drop.sy});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: drop.sx, y: drop.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx + 12, y: drop.sy,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(100);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.tx, y: drop.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(120);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.ghost").length'), 0,
|
||||
'pool drop does not preview an invented lane');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-cel.ghost").length'), 1,
|
||||
'pool drop previews inside the existing lane');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: drop.tx, y: drop.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
s = await shot();
|
||||
assert.equal(Object.keys(laneSymbols(s)[0].nodes).length, 1,
|
||||
`the dropped clip remains in the explicit lane: ${JSON.stringify(s)}`);
|
||||
assert.equal(laneSymbols(s).length, 1, 'the drop creates no extra lane');
|
||||
|
||||
const before = await tabs();
|
||||
const tabChips = () => evaluate('document.querySelectorAll(".tabs .tab").length');
|
||||
const chipsBefore = await tabChips();
|
||||
await doubleClick('.tl-track .tl-cel');
|
||||
const after = await tabs();
|
||||
assert.equal(after.tabs.length, before.tabs.length + 1,
|
||||
`double-clicking a clip opens the symbol it places, as the pool row does: ${JSON.stringify(after)}`);
|
||||
assert(!before.tabs.includes(after.open) && after.tabs.includes(after.open),
|
||||
`the opened symbol is the one in front: ${JSON.stringify(after)}`);
|
||||
assert.equal(await tabChips(), chipsBefore + 1,
|
||||
'the opened symbol is drawn as one more tab');
|
||||
assert.equal(await evaluate('document.querySelectorAll("#app > *").length'), 1,
|
||||
'opening from the timeline leaves the editor standing: a stale node selection ' +
|
||||
'pointing into the symbol just left used to throw and unmount it');
|
||||
// A second explicit lane is a sibling in the open symbol even though the
|
||||
// first remains aimed. Move the clip between their linear tracks.
|
||||
await clickNew('lane');
|
||||
s = await shot();
|
||||
assert.equal(laneSymbols(s).length, 2, 'a second explicit command creates a second lane');
|
||||
const move = await evaluate(`(() => {
|
||||
const tracks = [...document.querySelectorAll('.tl-track')].filter(t => t.arthurLane);
|
||||
const from = tracks.find(t => t.querySelector('.tl-cel'));
|
||||
const to = tracks.find(t => t !== from);
|
||||
const cel = from?.querySelector('.tl-cel');
|
||||
if (!cel || !to) return null;
|
||||
const a = cel.getBoundingClientRect(), b = to.getBoundingClientRect();
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * .12, ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
assert(move, 'two explicit lanes and a source clip are available');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: move.sx, y: move.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: move.tx, y: move.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(150);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: move.tx, y: move.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
s = await shot();
|
||||
assert.deepEqual(laneSymbols(s).map(x => Object.keys(x.nodes).length).sort(), [0, 1],
|
||||
'a clip body moves from one explicit lane to the other');
|
||||
assert.equal(errors.length, 0, JSON.stringify(errors));
|
||||
console.log('PASS: generic lanes preview, rename, move, place, open, trim, roll, and ripple clips');
|
||||
console.log('PASS: symbols are ordinary; explicit lanes rename, accept drops, and exchange clips');
|
||||
} finally {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ id: 999999, method: 'Browser.close' }));
|
||||
await sleep(350);
|
||||
}
|
||||
ws?.close();
|
||||
chrome.kill();
|
||||
await new Promise(resolve => { if (chrome.exitCode !== null || chrome.signalCode !== null) resolve(); else chrome.once('exit', resolve); });
|
||||
if (ws?.readyState === WebSocket.OPEN) ws.close();
|
||||
chrome.kill('SIGTERM');
|
||||
await new Promise(resolve => chrome.once('exit', resolve));
|
||||
try {
|
||||
rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
} catch (error) {
|
||||
console.warn(`Temporary browser profile retained at ${profile}: ${error.code}`);
|
||||
if (error.code !== 'ENOTEMPTY') throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue