mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Generate component swaps and variant switches
Generate swaps and variant switches across three component levels. Track carried groups independently by main names, ancestor kinds and ordinal; retain authored swapped subtrees and predict orientation from old values. Replay shrunk failures through the async frontend with target and undo checks after every step. Follow authored swaps to their content main, without taking overrides from the position they replaced or the result. All four properties pass 5000 programs each, seed712. The fresh swap run passes 5009 assertions; library passes 5008. MVP and structure runs remain valid because later changes only affect swaps and variant content. Isolated stage check: 55 tests, 379 assertions. Full frontend: 1539 tests, 9312 assertions; full common JVM and registered JavaScript suites pass. AI-assisted-by: GPT-6
This commit is contained in:
parent
2099084f0b
commit
3f948c72db
@ -13,6 +13,8 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logic.duplicate :as cld]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.logic.variants :as clv]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.comp.files :as cf]
|
||||
[app.common.test-helpers.composable.comp.nodes :as n]
|
||||
[app.common.test-helpers.composable.comp.runner :as r]
|
||||
@ -81,7 +83,19 @@
|
||||
[(n/make-nested-component name depth)
|
||||
(n/instantiate-copy name)
|
||||
(n/instantiate-copy name)])))))
|
||||
(let [s (setup/empty-situation)]
|
||||
(let [s (setup/empty-situation)
|
||||
s (if (:variants opts)
|
||||
(reduce #(tm/apply-to %2 %1) s
|
||||
[(n/create-component "variant" setup/labeled-fill :size 100)
|
||||
(n/make-variant-container "random-variants"
|
||||
[["a" setup/labeled-fill]
|
||||
["b" (if (:variant-diff opts) "#ff0000" setup/labeled-fill)]]
|
||||
:size 100)
|
||||
(n/make-nested-component-with-variant "variant" "random-variants" "a")
|
||||
(n/make-nested-component "variant" 1)
|
||||
(n/instantiate-copy "variant")
|
||||
(n/instantiate-copy "variant")])
|
||||
s)]
|
||||
(cond-> s
|
||||
(:text opts)
|
||||
(tm/with-file (-> (thtx/add-text-component-with-copy (tm/file s) [[["Hello world"]]])
|
||||
@ -125,9 +139,25 @@
|
||||
(cld/allow-duplicate? objects shape))
|
||||
:nest (and (top-copy shape) (not (overridden (:id shape))))
|
||||
:delete (ctk/main-instance? shape)
|
||||
(:swap :switch) (and (copy-head shape) (live-component shape))
|
||||
false))
|
||||
(tree situation))]
|
||||
(case kind
|
||||
(:swap :switch)
|
||||
(vec (for [entry eligible
|
||||
destination (tree situation)
|
||||
:let [shape (:shape entry)
|
||||
main (:shape destination)
|
||||
origin (get-in file [:data :components (:component-id shape)])
|
||||
component (get-in file [:data :components (:component-id main)])]
|
||||
:when (and (ctk/main-instance? main)
|
||||
(not= (:component-id shape) (:component-id main))
|
||||
(or (= kind :swap)
|
||||
(and (:variant-id origin)
|
||||
(= (:variant-id origin) (:variant-id component))))
|
||||
(not (clv/swap-nesting-loop? objects shape (:data file) (:id component))))]
|
||||
(assoc entry :destination destination
|
||||
:variant-value (get-in component [:variant-properties 0 :value]))))
|
||||
:restore (->> (vals (get-in file [:data :components]))
|
||||
(filter :deleted)
|
||||
(sort-by (comp str :id))
|
||||
@ -168,6 +198,7 @@
|
||||
:name (str "Label " (mod (or value 0) 3)))
|
||||
op (case kind
|
||||
:sync nil
|
||||
:library-sync (when (seq (tm/aux-files situation)) (n/sync-from-library))
|
||||
:undo (when (pos? (r/undo-depth situation)) (n/undo))
|
||||
:redo (when (pos? (r/redo-depth situation)) (n/redo))
|
||||
(when target
|
||||
@ -189,6 +220,8 @@
|
||||
:duplicate (n/duplicate target)
|
||||
:nest (n/nest-copy target (constantly (get-in selected [:parent :id])))
|
||||
:delete (n/delete-component target)
|
||||
:swap (n/swap-shape target (constantly (get-in selected [:destination :shape :component-id])))
|
||||
:switch (n/switch-variant target (:variant-value selected))
|
||||
:restore (n/restore-component (n/delete-component target)))))]
|
||||
{:op op :shape (:shape selected) :property property :value value
|
||||
:trace (cond-> {:kind kind :tuple tuple
|
||||
@ -243,6 +276,14 @@
|
||||
:duplicate (shadow/duplicate model objects id
|
||||
(:objects (thf/current-page (tm/file after)))
|
||||
(:new-head (n/applied-result op (tm/file before) (tm/file after))))
|
||||
(:swap :switch)
|
||||
(let [after-objects (all-objects after)
|
||||
old-main (get-in (cf/all-files before) [(:component-file shape) :data :components
|
||||
(:component-id shape) :main-instance-id])
|
||||
head (get after-objects id)
|
||||
new-main (get-in (cf/all-files after) [(:component-file head) :data :components
|
||||
(:component-id head) :main-instance-id])]
|
||||
(shadow/swap model objects after-objects id old-main new-main (= kind :switch)))
|
||||
(:resize :rotate)
|
||||
(let [shape-after (tm/shape-by-id after id)
|
||||
group :geometry-group]
|
||||
@ -379,3 +420,13 @@
|
||||
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
|
||||
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
|
||||
[:detach 0] [:undo] [:redo]] ops)}))
|
||||
|
||||
(def swap-program-gen
|
||||
(gen/let [components (gen/choose 2 3)
|
||||
differ? gen/boolean
|
||||
ops (gen/vector (gen/tuple (gen/elements (into structure-kinds [:swap :switch]))
|
||||
gen/nat gen/nat gen/nat gen/nat) 0 20)]
|
||||
{:world [components 2 {:variants true :variant-diff differ?}]
|
||||
:ops (into [[:edit 0 0 1 1] [:switch 0] [:switch 0]
|
||||
[:swap 0] [:undo] [:redo] [:reset 0]
|
||||
[:edit 0 1 1 1] [:update-main 0] [:sync 1]] ops)}))
|
||||
|
||||
@ -252,3 +252,110 @@
|
||||
groups (cond-> groups ordinary? (dissoc :swap-group))]
|
||||
(if (seq groups) (assoc result id groups) (dissoc result id))))
|
||||
model model))
|
||||
|
||||
(defn- subtree-shapes
|
||||
[objects id]
|
||||
(cons (get objects id) (mapcat #(subtree-shapes objects %) (:shapes (get objects id)))))
|
||||
|
||||
(defn- variant-paths
|
||||
[objects root-id]
|
||||
(letfn [(path [shape]
|
||||
(when-not (= root-id (:id shape))
|
||||
(conj (vec (path (get objects (:parent-id shape))))
|
||||
[(:name shape) (case (:type shape)
|
||||
(:frame :group) :container
|
||||
(:rect :circle :bool :path) :shape
|
||||
(:type shape))])))]
|
||||
(first
|
||||
(reduce (fn [[result counts] shape]
|
||||
(let [p (path shape)
|
||||
ordinal (get counts p 0)]
|
||||
[(assoc result (:id shape) [p ordinal]) (update counts p (fnil inc 0))]))
|
||||
[{} {}] (reverse (subtree-shapes objects root-id))))))
|
||||
|
||||
(defn swap
|
||||
"Predict switch carry from authored groups and the two mains. Matching
|
||||
uses main names and ancestor kinds; copy renames never change a match."
|
||||
[model before after root-id old-main-id new-main-id keep?]
|
||||
(let [old-paths (variant-paths before old-main-id)
|
||||
new-paths (variant-paths after new-main-id)
|
||||
old-by-path (into {} (map (fn [[id path]] [path (get before id)])) old-paths)
|
||||
source (fn [objects paths main-id original]
|
||||
(let [mains (into {} (keep (fn [shape]
|
||||
(when (:main-instance shape)
|
||||
[[(:component-file shape) (:component-id shape)] shape])))
|
||||
(vals objects))]
|
||||
(or
|
||||
(loop [shape original groups {} seen #{}]
|
||||
(cond
|
||||
(contains? paths (:id shape)) [shape groups]
|
||||
(or (nil? shape) (contains? seen (:id shape))) nil
|
||||
:else (recur (if (contains? (get model (:id shape)) :swap-group)
|
||||
(get mains [(:component-file shape) (:component-id shape)])
|
||||
(get objects (:shape-ref shape)))
|
||||
(merge (get model (:id shape)) groups)
|
||||
(conj seen (:id shape)))))
|
||||
;; A swapped root refers to its position, while its content
|
||||
;; and authored overrides belong to its own component.
|
||||
(when (= root-id (:id original))
|
||||
[(get objects main-id) (get model root-id)]))))
|
||||
old-copies (into {} (keep (fn [shape]
|
||||
(when-let [[main groups] (source before old-paths old-main-id shape)]
|
||||
[(get old-paths (:id main)) [shape main groups]])))
|
||||
(subtree-shapes before root-id))
|
||||
cleared (clear-subtree model before root-id)
|
||||
result
|
||||
(if-not keep?
|
||||
cleared
|
||||
(reduce
|
||||
(fn [result shape]
|
||||
(if (and (not= root-id (:id shape)) (contains? before (:id shape)))
|
||||
;; A switch keeps an authored swapped subtree with its ids.
|
||||
(cond-> result (seq (get model (:id shape)))
|
||||
(assoc (:id shape) (get model (:id shape))))
|
||||
(if-let [[main _] (source after new-paths new-main-id shape)]
|
||||
(let [path (get new-paths (:id main))
|
||||
[old old-main groups] (get old-copies path)
|
||||
old-main (or old-main (get old-by-path path))]
|
||||
(reduce-kv
|
||||
(fn [result group _]
|
||||
(let [attrs (get attributes group)
|
||||
carried (filter (fn [attr]
|
||||
(let [old-value (get old-main attr)
|
||||
new-value (get main attr)
|
||||
agree? (if (and (= group :geometry-group)
|
||||
(number? old-value) (number? new-value))
|
||||
(mth/close? old-value new-value)
|
||||
(= old-value new-value))]
|
||||
(and agree? (not= (get old attr) new-value)))) attrs)]
|
||||
(cond-> result
|
||||
(seq carried)
|
||||
(assoc-in [(:id shape) group]
|
||||
(merge (values main group) (select-keys old carried))))))
|
||||
result (dissoc groups :swap-group)))
|
||||
result)))
|
||||
cleared (subtree-shapes after root-id)))
|
||||
root (get after root-id)
|
||||
old-root (get before root-id)
|
||||
parent (get after (:parent-id root))
|
||||
position (get after (:shape-ref root))
|
||||
main (get after new-main-id)
|
||||
main-parent (get after (:parent-id main))
|
||||
baseline (if (:shape-ref parent) position main)
|
||||
baseline-parent (if (:shape-ref parent)
|
||||
(get after (:parent-id position)) main-parent)
|
||||
rotation (mod (+ (or (:rotation baseline) 0)
|
||||
(- (or (:rotation parent) 0) (or (:rotation baseline-parent) 0))) 360)
|
||||
flip (fn [key]
|
||||
(odd? (count (filter identity [(get baseline key) (get parent key)
|
||||
(get baseline-parent key)]))))
|
||||
retained-orientation? (or (not (mth/close? (mod (or (:rotation old-root) 0) 360) rotation 0.01))
|
||||
(not= (boolean (:flip-x old-root)) (flip :flip-x))
|
||||
(not= (boolean (:flip-y old-root)) (flip :flip-y)))]
|
||||
(cond-> result
|
||||
retained-orientation?
|
||||
(assoc-in [root-id :geometry-group]
|
||||
(merge (or (get-in result [root-id :geometry-group]) (values main :geometry-group))
|
||||
(select-keys old-root [:rotation :flip-x :flip-y])))
|
||||
(and (:shape-ref parent) (not= (:component-id root) (:component-id position)))
|
||||
(assoc-in [root-id :swap-group] {}))))
|
||||
|
||||
@ -118,6 +118,57 @@
|
||||
(t/is (= 1 (count (r/pending-components partial))))
|
||||
(t/is (empty? (r/pending-components settled)))))
|
||||
|
||||
(t/deftest swaps-and-variant-switches-run-in-a-three-level-world
|
||||
(let [program {:world [2 2 {:variants true}]
|
||||
:ops [[:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:switch 1] [:sync 0]]}
|
||||
result (cg/run program)]
|
||||
(t/is (pos? (get-in result [:executed :swap] 0)))
|
||||
(t/is (pos? (get-in result [:executed :switch] 0)))
|
||||
(t/is (= (:trace result) (:trace (cg/run program (:trace result)))))))
|
||||
|
||||
(t/deftest variant-carry-follows-the-two-mains
|
||||
(doseq [differ? [false true]]
|
||||
(let [spec [2 2 {:variants true :variant-diff differ?}]
|
||||
initial (cg/initial-state spec)
|
||||
situation (:situation initial)
|
||||
head (:shape (cg/resolve-op situation {} [:switch 0]))
|
||||
child (first (:shapes head))
|
||||
index (first (keep-indexed #(when (= child (get-in %2 [:shape :id])) %1)
|
||||
(cg/candidates situation {} :edit 1)))
|
||||
result (cg/run {:world spec :ops [[:edit index 0 2 1] [:switch 0] [:sync 0]
|
||||
[:undo] [:redo]]})
|
||||
groups (mapcat keys (vals (:model result)))]
|
||||
(t/is (some? index))
|
||||
(t/is (= (not differ?) (boolean (some #{:fill-group} groups)))))))
|
||||
|
||||
(t/deftest random-swap-compositions
|
||||
(let [coverage (atom {})
|
||||
failure (atom nil)]
|
||||
(smt/check!
|
||||
(smt/for [program cg/swap-program-gen]
|
||||
(cg/check-program program coverage failure))
|
||||
:max-size 30)
|
||||
(println "Executed swap operations:" (pr-str @coverage))
|
||||
(doseq [kind [:swap :switch :edit :reset :undo :redo]]
|
||||
(t/is (pos? (get @coverage kind 0)) (str "No executed " kind)))))
|
||||
|
||||
(t/deftest random-renames-leave-variant-management-to-its-own-operations
|
||||
(let [situation (cg/world [2 2 {:variants true}])
|
||||
file (tm/file situation)]
|
||||
(doseq [index (range (count (cg/tree situation)))
|
||||
:let [shape (:shape (cg/resolve-op situation {} [:edit index 2 1]))]]
|
||||
(t/is (not (:is-variant-container shape)))
|
||||
(t/is (nil? (:variant-id shape)))
|
||||
(t/is (nil? (get-in file [:data :components (:component-id shape) :variant-id]))))))
|
||||
|
||||
(t/deftest swap-after-updating-a-rotated-main-is-already-fully-synced
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1]
|
||||
[:update-main 0] [:sync 1] [:rotate 0 0 0 0]
|
||||
[:update-main 0 0 0 0] [:swap 0 0 0 0] [:restore 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest detach-from-a-deleted-near-main-inherits-only-the-skipped-level
|
||||
(t/is (map? (cg/run {:world [3 2 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -145,6 +196,13 @@
|
||||
[:detach 0] [:undo] [:redo] [:rotate 2 0 0 0]
|
||||
[:resize 7 0 0 0] [:duplicate 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest pushed-swap-followed-by-nesting-undoes-exactly
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1]
|
||||
[:update-main 0] [:sync 1] [:swap 0 0 0 0]
|
||||
[:switch 1 0 0 0] [:update-main 0 0 0 0] [:nest 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest updating-a-rotated-outer-main-composes-authored-child-orientation
|
||||
(t/is (map? (cg/run {:world [2 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -155,6 +213,14 @@
|
||||
[:resize 0 0 0 0] [:nest 0 0 0 0] [:rotate 4 0 0 0]
|
||||
[:detach 3 0 0 0] [:update-main 4 0 0 0]]}))))
|
||||
|
||||
(t/deftest swapping-main-subtrees-with-several-children-undoes-their-order
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1]
|
||||
[:update-main 0] [:sync 1] [:add-child 1 0 0 0]
|
||||
[:add-child 1 0 0 0] [:add-child 1 0 0 0]
|
||||
[:swap 0 0 0 0] [:swap 0 0 0 0] [:text 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest minimum-size-parent-orientation-does-not-amplify-oracle-drift
|
||||
(t/is (map? (cg/run {:world [3 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -185,6 +251,46 @@
|
||||
(t/is (false? (shadow/same-value? (:child after) :geometry-group
|
||||
(get-in model [:child :geometry-group]))))))
|
||||
|
||||
(t/deftest switch-shadow-tolerates-main-size-noise-but-rejects-real-size-changes
|
||||
(doseq [[difference expected] [[1e-12 80] [1 nil]]]
|
||||
(let [main {:id :old :name "Board" :type :frame :width 100 :height 100 :rotation 0}
|
||||
copy (assoc main :id :copy :shape-ref :old :height 80 :parent-id :page)
|
||||
target (assoc main :id :new :height (+ 100 difference))
|
||||
result (shadow/swap {:copy {:geometry-group (shadow/values copy :geometry-group)}}
|
||||
{:old main :copy copy}
|
||||
{:new target :copy (assoc copy :shape-ref :new)}
|
||||
:copy :old :new true)]
|
||||
(t/is (= expected (get-in result [:copy :geometry-group :height]))))))
|
||||
|
||||
(t/deftest switch-shadow-follows-authored-content-through-a-swapped-near-main
|
||||
(doseq [swapped? [false true]]
|
||||
(let [main {:id :a :name "Board" :type :frame :main-instance true
|
||||
:component-id :a-component :component-file :local :fills [:white]}
|
||||
before {:a main
|
||||
:b (assoc main :id :b :component-id :b-component)
|
||||
:position (assoc main :id :position :main-instance false :shape-ref :b
|
||||
:component-id :b-component :fills [:blue])
|
||||
:near (assoc main :id :near :main-instance false :shape-ref (if swapped? :position :a)
|
||||
:parent-id :outer-main :fills [:green])
|
||||
:parent {:id :parent :shape-ref :outer-main}
|
||||
:copy (assoc main :id :copy :main-instance false :shape-ref :near
|
||||
:parent-id :parent :fills [:green])}
|
||||
after (assoc before :copy (assoc (:copy before) :component-id :b-component
|
||||
:fills [:wrong]))
|
||||
model {:near (cond-> {:fill-group {:fills [:green]}}
|
||||
swapped? (assoc :swap-group {}))
|
||||
:position {:fill-group {:fills [:blue]}}}
|
||||
result (shadow/swap model before after :copy :a :b true)
|
||||
expected (get-in result [:copy :fill-group])]
|
||||
(t/is (= [:green] (:fills expected)))
|
||||
(t/is (false? (shadow/same-value? (:copy after) :fill-group expected))))))
|
||||
|
||||
(t/deftest rotated-swapped-head-keeps-its-position-anchor-with-a-different-content-size
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1] [:update-main 0]
|
||||
[:sync 1] [:swap 17 0 0 0] [:resize 0 0 0 0] [:rotate 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest pushed-swap-shadow-predicts-orientation-without-reading-the-new-child
|
||||
(let [before {:copy {:id :copy :shape-ref :main :shapes [:child]}
|
||||
:main {:id :main :rotation 0}
|
||||
@ -222,6 +328,47 @@
|
||||
(t/is (= {:fills [:white] :hide-fill-on-export nil} (get-in model [id :fill-group])))
|
||||
(t/is (false? (shadow/same-value? (get after id) :fill-group (get-in model [id :fill-group])))))))
|
||||
|
||||
(t/deftest switch-shadow-keeps-inherited-orientation-even-if-the-result-loses-it
|
||||
(let [before {:page {:id :page}
|
||||
:main-a {:id :main-a :parent-id :page :name "A" :rotation 45}
|
||||
:main-b {:id :main-b :parent-id :page :name "B" :rotation 0}
|
||||
:copy {:id :copy :parent-id :page :shape-ref :main-a :rotation 45}}
|
||||
after (assoc before :copy (assoc (:copy before) :shape-ref :main-b :rotation 0))
|
||||
model (shadow/swap {} before after :copy :main-a :main-b true)
|
||||
expected (get-in model [:copy :geometry-group])]
|
||||
(t/is (= 45 (:rotation expected)))
|
||||
(t/is (false? (shadow/same-value? (:copy after) :geometry-group expected)))))
|
||||
|
||||
(t/deftest switch-shadow-combines-carried-size-with-retained-orientation
|
||||
(let [before {:page {:id :page}
|
||||
:main-a {:id :main-a :parent-id :page :name "A" :width 100 :height 100 :rotation 45}
|
||||
:main-b {:id :main-b :parent-id :page :name "B" :width 100 :height 100 :rotation 0}
|
||||
:copy {:id :copy :parent-id :page :shape-ref :main-a
|
||||
:width 100 :height 80 :rotation 90}}
|
||||
after (assoc before :copy (assoc (:copy before) :shape-ref :main-b :rotation 0))
|
||||
model (shadow/swap {:copy {:geometry-group (shadow/values (:copy before) :geometry-group)}}
|
||||
before after :copy :main-a :main-b true)
|
||||
expected (get-in model [:copy :geometry-group])]
|
||||
(t/is (= 80 (:height expected)))
|
||||
(t/is (= 90 (:rotation expected)))
|
||||
(t/is (false? (shadow/same-value? (:copy after) :geometry-group expected)))))
|
||||
|
||||
(t/deftest switch-keeps-retained-orientation-together-with-carried-size
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
|
||||
[:swap 0 0 0 0] [:delete 0 0 0 0] [:rotate 0 0 0 0]
|
||||
[:nest 0 0 0 0] [:detach 0 0 0 0] [:update-main 0 0 0 0]
|
||||
[:switch 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest switching-an-untouched-rotated-copy-survives-full-sync
|
||||
(t/is (map? (cg/run {:world [3 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
|
||||
[:edit 11 0 0 0] [:rotate 2 0 0 0] [:nest 3 0 0 0] [:resize 3 0 0 0]
|
||||
[:nest 3 0 0 0] [:swap 10 0 0 0] [:resize 3 0 0 0]
|
||||
[:update-main 5 0 0 0] [:switch 0 0 0 0] [:detach 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
|
||||
(t/is (map? (cg/run {:world [3 0 {:text true :layout :flex}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -234,3 +381,13 @@
|
||||
(t/deftest updating-a-main-after-successive-rotations-keeps-child-flips
|
||||
(t/is (map? (cg/run {:world [2 1]
|
||||
:ops [[:rotate 1 0 0 0] [:rotate 1 0 1 0] [:update-main 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-update-main-settles-a-turned-swapped-position
|
||||
(let [result (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
|
||||
[:delete 8 0 0 0] [:swap 15 0 0 0] [:edit 14 0 0 0]
|
||||
[:resize 4 0 0 0] [:resize 0 0 0 0] [:nest 4 0 0 0]
|
||||
[:rotate 5 0 0 0] [:update-main 14 0 0 0] [:duplicate 0 0 0 0]]})]
|
||||
(t/is (pos? (get-in result [:executed :update-main] 0)))
|
||||
(t/is (pos? (get-in result [:executed :rotate] 0)))))
|
||||
|
||||
@ -99,6 +99,10 @@
|
||||
(t/deftest ^:async nesting-restores-default-constraints-on-undo
|
||||
(await (replay {:world [2 0] :ops [[:nest 0]]})))
|
||||
|
||||
(t/deftest ^:async swap-programs-replay-through-the-frontend
|
||||
(doseq [seed [712 20261022]]
|
||||
(await (replay (gen/generate cg/swap-program-gen 5 seed)))))
|
||||
|
||||
(t/deftest ^:async partial-sync-geometry-boundary-shrunk-regression
|
||||
(await (replay {:world [2 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -113,6 +117,14 @@
|
||||
:ops [[:duplicate 0] [:nest 0] [:rotate 0 0 0]
|
||||
[:edit 0 0 1 1] [:update-main 0]]})))
|
||||
|
||||
(t/deftest ^:async swapping-main-subtrees-with-several-children-undoes-their-order
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1]
|
||||
[:update-main 0] [:sync 1] [:add-child 1 0 0 0]
|
||||
[:add-child 1 0 0 0] [:add-child 1 0 0 0]
|
||||
[:swap 0 0 0 0] [:swap 0 0 0 0] [:text 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async updating-a-rotated-outer-main-composes-authored-child-orientation
|
||||
(await (replay {:world [2 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -142,6 +154,12 @@
|
||||
[:detach 0] [:undo] [:redo] [:resize 0 0 0 0]
|
||||
[:delete 0 0 0 0] [:rotate 0 0 0 0] [:detach 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async rotated-swapped-head-keeps-its-position-anchor-with-a-different-content-size
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1] [:update-main 0]
|
||||
[:sync 1] [:swap 17 0 0 0] [:resize 0 0 0 0] [:rotate 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
|
||||
(await (replay {:world [3 0 {:text true :layout :flex}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -163,3 +181,27 @@
|
||||
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
|
||||
[:detach 0] [:undo] [:redo] [:resize 1 0 0 0]
|
||||
[:nest 2 0 0 0] [:rotate 0 0 3 0] [:edit 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async switching-an-untouched-rotated-copy-survives-full-sync
|
||||
(await (replay {:world [3 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
|
||||
[:edit 11 0 0 0] [:rotate 2 0 0 0] [:nest 3 0 0 0] [:resize 3 0 0 0]
|
||||
[:nest 3 0 0 0] [:swap 10 0 0 0] [:resize 3 0 0 0]
|
||||
[:update-main 5 0 0 0] [:switch 0 0 0 0] [:detach 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async switch-keeps-retained-orientation-together-with-carried-size
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
|
||||
[:swap 0 0 0 0] [:delete 0 0 0 0] [:rotate 0 0 0 0]
|
||||
[:nest 0 0 0 0] [:detach 0 0 0 0] [:update-main 0 0 0 0]
|
||||
[:switch 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-update-main-settles-a-turned-swapped-position
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
|
||||
[:delete 8 0 0 0] [:swap 15 0 0 0] [:edit 14 0 0 0]
|
||||
[:resize 4 0 0 0] [:resize 0 0 0 0] [:nest 4 0 0 0]
|
||||
[:rotate 5 0 0 0] [:update-main 14 0 0 0] [:duplicate 0 0 0 0]]})))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user