diff --git a/common/src/app/common/test_helpers/composable/gen.cljc b/common/src/app/common/test_helpers/composable/gen.cljc index 5f805289a9..6f758eebf4 100644 --- a/common/src/app/common/test_helpers/composable/gen.cljc +++ b/common/src/app/common/test_helpers/composable/gen.cljc @@ -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)})) diff --git a/common/src/app/common/test_helpers/composable/shadow.cljc b/common/src/app/common/test_helpers/composable/shadow.cljc index 98f7c29fce..065139af60 100644 --- a/common/src/app/common/test_helpers/composable/shadow.cljc +++ b/common/src/app/common/test_helpers/composable/shadow.cljc @@ -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] {})))) diff --git a/common/test/common_tests/logic/composable_gen_test.cljc b/common/test/common_tests/logic/composable_gen_test.cljc index 6f0377c55c..b9a422d65b 100644 --- a/common/test/common_tests/logic/composable_gen_test.cljc +++ b/common/test/common_tests/logic/composable_gen_test.cljc @@ -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))))) diff --git a/frontend/test/frontend_tests/composable_tests/generated_test.cljs b/frontend/test/frontend_tests/composable_tests/generated_test.cljs index a8e7cd9274..57e21de55f 100644 --- a/frontend/test/frontend_tests/composable_tests/generated_test.cljs +++ b/frontend/test/frontend_tests/composable_tests/generated_test.cljs @@ -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]]})))