diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 585e7ba745..ec3ac679f0 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -62,6 +62,9 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w - Children of a resized or transformed shape: derived (expected geometry), untouched, and still follow later main edits. Code: a transform touches only the shapes the user transformed (`clp/calculate-ignore-tree` takes their ids; the frontend keeps them in `:workspace-modifiers-ids`); sync places the main subtree of a copy shape with its own geometry like that shape before walking its children. - Layout-owned (never touched; sync writes, then reflow): x/y of non-absolute children of flex/grid frames; width/height on axes sized fill, or auto on a layout frame. - Grow-owned (never touched; sync writes, then re-measure on SVG and WASM): width+height of auto-width text, height of auto-height text. Every editor and resize path writes the measured size as derived; on WASM `sync-file` re-measures the auto-grow texts of the current page it wrote (texts on other pages keep the synced size until measured). +- Transform settlement corrects geometry on the existing copy subtree + (F223). Its sync proposal must not pull pending library content, styles + or structure. Path content can carry geometry; text content cannot. - `set-shape-attr`'s `ignore-geometry` does not cover `:width`/`:height`; a derived size write needs `ignore-touched` (transforms get it from the ignore tree). - Repairs are not overrides: `files/repair` writes with `ignore-touched`; only the text repairs set groups, explicitly. diff --git a/.serena/memories/common/testing.md b/.serena/memories/common/testing.md index 56d6611985..34f2069897 100644 --- a/.serena/memories/common/testing.md +++ b/.serena/memories/common/testing.md @@ -120,6 +120,15 @@ Settlement also checks partial/full sync equivalence, idempotence and exact undo/redo snapshots. Library routing in `comp/files` applies both the working file and auxiliary file changes, then cascades both in the same undo group. +Stale-library and dependency generation includes source transforms on main +roots/children, letters and all twelve text style units. `:library-text true` +adds a separate source text component without reusing consumer shape IDs. +Source edits keep consumer history separate and copies stale until an +acceptance or command that explicitly refreshes that subtree. Requested +source geometry/text checks use the source editor's before-state and intent; +dropped source edits must fail I9. `common-tests.logic.source-gen-test` +covers the action matrix and independent source/consumer undo histories. + Backend parity uses `oracle/canonical` and `oracle/equivalent?`: IDs become file/page/tree paths, flip nil/false values become booleans, and derived layout/text dimensions and text diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 721c403b57..bc2b49c971 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -333,11 +333,17 @@ changes) changes))) +(def ^:private placement-geometry-attrs + #{:x :y :width :height :selrect :points :transform :transform-inverse + :rotation :flip-x :flip-y :content-scale}) + (defn generate-sync-transformed-placement "Settle copy descendants after a reflection and untouched swapped heads after a parent turn, in the transform's change group. Reflection can leave descendant flip flags in another decomposition; swapped content - can have a different size from its position frame." + can have a different size from its position frame. Keep this correction + within existing geometry: transforming a stale library copy must not + accept pending content, style or structural changes." [changes file libraries page before ids] (let [objects (pcb/get-objects changes) orientation (fn [shape] @@ -362,11 +368,21 @@ (let [objects (pcb/get-objects changes) page (assoc page :objects objects) file (assoc-in file [:data :pages-index (:id page) :objects] objects) - container (ctn/make-container page :page)] - (-> changes - (pcb/with-container container) - (generate-sync-shape-direct file (assoc libraries (:id file) file) - container id false)))) + container (ctn/make-container page :page) + changes (pcb/with-container changes container) + proposal (generate-sync-shape-direct changes file (assoc libraries (:id file) file) + container id false) + placed (pcb/get-objects proposal)] + (pcb/update-shapes + changes (cfh/get-children-ids-with-self objects id) + (fn [shape] + (let [expected (get placed (:id shape)) + attrs (cond-> placement-geometry-attrs + (= :path (:type shape)) (conj :content))] + (if (= (:type shape) (:type expected)) + (merge shape (select-keys expected attrs)) + shape))) + {:ignore-touched true}))) changes roots))) (defn- sync-copy-shape diff --git a/common/src/app/common/test_helpers/composable/gen.cljc b/common/src/app/common/test_helpers/composable/gen.cljc index e36a922ac3..0527796b99 100644 --- a/common/src/app/common/test_helpers/composable/gen.cljc +++ b/common/src/app/common/test_helpers/composable/gen.cljc @@ -75,6 +75,20 @@ [files] (sort-by (fn [[id file]] [(:name file) (str (thi/label id))]) files)) +(defn- with-source-text + [file] + (-> file + (tho/add-frame-with-text :library-text-root :library-text-child "Library text" + :child-params {:grow-type :auto-height}) + (thc/make-component :library-text-component :library-text-root) + (thc/instantiate-component :library-text-component :library-text-copy) + (ths/add-sample-typography :generated-typography + :font-id "gfont-roboto" :font-family "Roboto" + :font-variant-id "700italic" :font-weight "700" + :font-style "italic" :font-size "22" + :line-height "1.6" :letter-spacing "2" + :text-transform "uppercase"))) + (defn- with-library [situation opts] (let [library (-> (tm/make-situation (thf/sample-file :random-library :name "random-library" :is-shared true)) @@ -87,7 +101,8 @@ s)) (as-> s (if (and (:library-variants opts) (:rich-variants opts)) (variants/diversify s "library-variants") s)) - tm/file) + tm/file + (cond-> (:library-text opts) with-source-text)) dependent (when (:library-dependency opts) (-> (tm/make-situation (thf/sample-file :dependent-library :name "dependent-library" :is-shared true)) (as-> s (tm/apply-to (n/create-component "library-dependent" setup/labeled-fill :size 100) s)) @@ -269,10 +284,10 @@ (def library-source-kinds #{:library-edit :library-add-child :library-remove-child :library-delete :library-restore - :library-undo :library-redo :library-accept}) + :library-undo :library-redo :library-accept :library-transform :library-text :library-text-style}) (defn- library-candidates - [situation kind] + [situation kind scope] (vec (for [[library-index [library-id library]] (map-indexed vector (ordered-files (tm/aux-files situation))) @@ -299,6 +314,15 @@ (case kind (:library-edit :library-remove-child) (and (= :rect (:type shape)) (nil? (:shape-ref shape))) + :library-transform + (and (not= :text (:type shape)) (nil? (:shape-ref shape)) + (if (even? (or scope 0)) + (ctk/main-instance? shape) + (and (not (ctk/main-instance? shape)) + (some ctk/main-instance? + (cfh/get-parents (:objects (thf/current-page library)) (:id shape)))))) + (:library-text :library-text-style) + (and (= :text (:type shape)) (nil? (:shape-ref shape))) (:library-add-child :library-delete) (ctk/main-instance? shape))) (tree (tm/with-file situation library))))] (-> entry @@ -360,7 +384,7 @@ (let [index (or index 0) [style-property style-values] (nth style-edits (mod (or property 0) (count style-edits))) items (cond->> (if (contains? library-source-kinds kind) - (library-candidates situation kind) + (library-candidates situation kind scope) (candidates situation model (if (= kind :style) :edit kind) scope)) (= kind :style) (filter (fn [{:keys [shape]}] @@ -399,15 +423,15 @@ (when target (case kind (:edit :style) (n/change-property target property value) - :transform (let [action (nth [:move :width :height :rotate :flip-x :flip-y] - (mod (or (nth tuple 2 nil) 0) 6)) - value (or (nth tuple 3 nil) 0)] - (n/transform-shape target action - (case action - :move [(* 5 (inc (mod value 4))) (* -3 (inc (mod value 3)))] - (:width :height) (+ 80 (* 20 (mod value 6))) - :rotate (* 45 (inc (mod value 4))) - nil))) + (:transform :library-transform) (let [action (nth [:move :width :height :rotate :flip-x :flip-y] + (mod (or (nth tuple 2 nil) 0) 6)) + value (or (nth tuple 3 nil) 0)] + (n/transform-shape target action + (case action + :move [(* 5 (inc (mod value 4))) (* -3 (inc (mod value 3)))] + (:width :height) (+ 80 (* 20 (mod value 6))) + :rotate (* 45 (inc (mod value 4))) + nil))) :library-edit (n/change-property target property value) :library-add-child (n/add-child target (keyword (str "library-generated-" (count (:applied situation)))) :width 20 :height 20 :x 10 :y 10) @@ -419,17 +443,21 @@ :detach (n/detach target) :resize (n/change-height target (+ 80 (* 20 (mod (or (nth tuple 3 nil) 0) 6)))) :rotate (n/rotate target (* 45 (inc (mod (or (nth tuple 3 nil) 0) 4)))) - :text (let [text (thtx/view (:content (:shape selected))) - text (if (even? (or (nth tuple 2 nil) 0)) - (thtx/set-letters text (nth ["Hello world" "Hello there" "Hello\nworld"] (mod (or (nth tuple 3 nil) 0) 3))) - (thtx/restyle text :size (nth [14 18 24] (mod (or (nth tuple 3 nil) 0) 3))))] - (n/edit-text target :content text)) - :text-style (n/edit-text target :content - (edit-text-style (:shape selected) (nth tuple 2 nil) (nth tuple 3 nil)) - (when (and (= 8 (mod (or (nth tuple 2 nil) 0) (count text-style-edits))) - (odd? (or (nth tuple 3 nil) 0))) - {:typography {:id (thi/id :generated-typography) - :file-id (:id (tm/file situation))}})) + (:text :library-text) (let [text (thtx/view (:content (:shape selected))) + text (if (even? (or (nth tuple 2 nil) 0)) + (thtx/set-letters text (nth ["Hello world" "Hello there" "Hello\nworld"] (mod (or (nth tuple 3 nil) 0) 3))) + (thtx/restyle text :size (nth [14 18 24] (mod (or (nth tuple 3 nil) 0) 3))))] + (n/edit-text target :content text)) + (:text-style :library-text-style) + (let [typography? (and (= 8 (mod (or (nth tuple 2 nil) 0) (count text-style-edits))) + (odd? (or (nth tuple 3 nil) 0))) + file-id (or (:library-id selected) (:id (tm/file situation))) + content (cond-> (edit-text-style (:shape selected) (nth tuple 2 nil) (nth tuple 3 nil)) + (and typography? (:library-id selected)) + (thtx/restyle :typography-ref-file file-id))] + (n/edit-text target :content content + (when typography? + {:typography {:id (thi/id :generated-typography) :file-id file-id}}))) :add-child (n/add-child target (keyword (str "generated-" (count (:applied situation)))) :width 20 :height 20 :x 10 :y 10) :remove-child (n/remove-child target) @@ -589,7 +617,7 @@ modifiers (case kind :resize (ctm/change-size shape nil (:value op)) :rotate (n/geometry-modifiers shape :rotate (:angle op)) - :transform (n/geometry-modifiers shape (:action op) (:value op))) + (:transform :library-transform) (n/geometry-modifiers shape (:action op) (:value op))) tree (gm/set-objects-modifiers {(:id shape) {:modifiers modifiers}} objects)] (gsh/transform-shape shape (get-in tree [(:id shape) :modifiers])))) @@ -688,16 +716,19 @@ (> (r/undo-depth after) (r/undo-depth situation))) (update-in (conj coverage-path :undo-only) (fnil inc 0))) (update-in [:executed kind] (fnil inc 0)))] - (when (#{:resize :rotate :transform} kind) - (oracle/check-requested-geometry (:objects (thf/current-page (tm/file after))) - (tm/shape-by-id after (get-in resolved [:shape :id])) - (requested-geometry situation resolved kind))) + (when (#{:resize :rotate :transform :library-transform} kind) + (let [before (if library-id (source-context situation library-id) situation) + after (if library-id (source-context after library-id) after)] + (oracle/check-requested-geometry (:objects (thf/current-page (tm/file after))) + (tm/shape-by-id after (get-in resolved [:shape :id])) + (requested-geometry before resolved kind)))) (when (#{:swap :switch} kind) (let [head (tm/shape-by-id after (get-in resolved [:shape :id]))] (when-not (= (get-in resolved [:destination :component-id]) (:component-id head)) (oracle/fail :I9 {:phase :requested-component :kind kind})))) - (when (#{:text :text-style} kind) - (let [actual (tm/shape-by-id after (get-in resolved [:shape :id])) + (when (#{:text :text-style :library-text :library-text-style} kind) + (let [after (if library-id (source-context after library-id) after) + actual (tm/shape-by-id after (get-in resolved [:shape :id])) expected (thtx/normalize (:value op))] (when-not (= expected (thtx/view (:content actual))) (oracle/fail :I9 {:phase :requested-text :expected expected @@ -845,6 +876,14 @@ (contains? (set kinds) :text-style) (conj [2 (gen/fmap (fn [[index unit]] [[:text-style index unit 1]]) (gen/tuple gen/nat (gen/choose 0 (dec (count text-style-edits)))))]) + (contains? (set kinds) :library-transform) + (conj [2 (gen/fmap (fn [[index action scope]] [[:library-transform index action 4 scope]]) + (gen/tuple gen/nat (gen/choose 0 5) (gen/choose 0 1)))]) + (contains? (set kinds) :library-text-style) + (conj [2 (gen/fmap (fn [[index unit]] [[:library-text-style index unit 1]]) + (gen/tuple gen/nat (gen/choose 0 (dec (count text-style-edits)))))]) + (contains? (set kinds) :library-text) + (conj [2 (gen/fmap (fn [index] [[:library-text index 0 1]]) gen/nat)]) (every? (set kinds) [:undo :redo]) (conj [1 (gen/return [[:undo] [:redo]])]) (contains? (set kinds) :text) @@ -928,17 +967,18 @@ library-depth (gen/choose 1 2) layout (gen/elements [nil :flex :grid]) ops (operation-gen stale-library-kinds)] - {:world [2 depth {:library true :stale-libraries true :library-variants true + {:world [2 depth {:library true :stale-libraries true :library-variants true :library-text true :library-depth library-depth :variants true :rich-variants true :text true :text-styles true :layout layout :layout-copies true}] :ops ops})) (def dependency-kinds [:edit :reset :swap :sync :undo :redo :library-sync :library-edit :library-accept - :library-add-child :library-remove-child :library-delete :library-restore :library-undo :library-redo]) + :library-add-child :library-remove-child :library-delete :library-restore :library-undo :library-redo + :library-transform :library-text :library-text-style]) (def dependency-program-gen (gen/let [depth (gen/choose 0 1) ops (operation-gen dependency-kinds)] - {:world [2 depth {:library true :stale-libraries true :library-dependency true}] + {:world [2 depth {:library true :stale-libraries true :library-dependency true :library-text true}] :ops ops})) diff --git a/common/test/common_tests/logic/source_gen_test.cljc b/common/test/common_tests/logic/source_gen_test.cljc new file mode 100644 index 0000000000..d3f766de68 --- /dev/null +++ b/common/test/common_tests/logic/source_gen_test.cljc @@ -0,0 +1,93 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns common-tests.logic.source-gen-test + (:require + [app.common.test-helpers.composable.comp.runner :as r] + [app.common.test-helpers.composable.core :as tm] + [app.common.test-helpers.composable.gen :as cg] + [app.common.test-helpers.composable.oracle :as oracle] + [app.common.test-helpers.ids-map :as thi] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(def source-world + [2 1 {:library true :stale-libraries true :library-text true :text true :text-styles true}]) + +(t/deftest a-transform-does-not-accept-pending-library-content-or-structure + (doseq [[edit index] [[[:library-text 0 1 1] 8] + [[:library-text 0 0 1] 8] + [[:library-edit 0 0 1] 4] + [[:library-add-child 0] 4]]] + (t/is (map? (cg/run {:world [2 0 {:library true :stale-libraries true :library-text true}] + :ops [edit [:transform index 3 3 4] [:undo] [:redo] + [:library-sync]]})) + (str edit)))) + +(t/deftest source-transforms-keep-consumers-stale-until-acceptance + (doseq [action (range 6) scope [0 1]] + (let [initial (cg/initial-state source-world) + situation (:situation initial) + tuple [:library-transform 0 action 4 scope] + resolved (cg/resolve-op situation {} tuple) + owner (:library-id resolved)] + (t/is (some? (:op resolved)) (str "source transform " action ", scope " scope)) + (when (:op resolved) + (let [edited (-> initial (cg/step tuple) cg/settle-state) + source (get (oracle/snapshot (:situation edited)) owner)] + (t/is (not= (get (oracle/snapshot situation) owner) source)) + (t/is (= (dissoc (oracle/snapshot situation) owner) + (dissoc (oracle/snapshot (:situation edited)) owner))) + (t/is (zero? (r/undo-depth (:situation edited)))) + (t/is (map? (cg/run {:world source-world + :ops [tuple [:library-sync] [:undo] [:redo] + [:library-undo] [:library-redo] [:library-sync]]})))))))) + +(t/deftest source-text-edits-cover-letters-and-every-style-unit + (doseq [tuple (into [[:library-text 0 0 1]] + (map #(vector :library-text-style 0 % 1) (range 12)))] + (let [initial (cg/initial-state source-world) + situation (:situation initial) + resolved (cg/resolve-op situation {} tuple) + owner (:library-id resolved)] + (t/is (some? (:op resolved)) (str tuple)) + (when (:op resolved) + (let [edited (-> initial (cg/step tuple) cg/settle-state)] + (t/is (not= (get (oracle/snapshot situation) owner) + (get (oracle/snapshot (:situation edited)) owner))) + (t/is (= (dissoc (oracle/snapshot situation) owner) + (dissoc (oracle/snapshot (:situation edited)) owner))) + (t/is (zero? (r/undo-depth (:situation edited)))) + (t/is (map? (cg/run {:world source-world + :ops [tuple [:library-sync] [:undo] [:redo] + [:library-undo] [:library-redo] [:library-sync]]})))))))) + +(t/deftest source-intent-checks-reject-a-dropped-primary-edit + (doseq [tuple [[:library-transform 0 1 4 0] + [:library-transform 0 3 1 1] + [:library-text 0 0 1] + [:library-text-style 0 2 1]]] + (let [initial (cg/initial-state source-world)] + (t/is (= :I9 + (try + (with-redefs [r/step (fn [situation _] situation)] + (cg/step initial tuple)) + nil + (catch #?(:clj Exception :cljs js/Error) error + (:invariant (ex-data error))))) + (str "dropped source intent " tuple))))) + +(t/deftest source-text-fixture-does-not-reuse-consumer-shape-ids + (let [situation (cg/world source-world) + entries (cg/forest situation) + ids (map (comp :id :shape) entries) + source-texts (for [[_ library] (tm/aux-files situation) + entry (cg/tree (tm/with-file situation library)) + :when (= :text (get-in entry [:shape :type]))] + entry)] + (t/is (= (count ids) (count (set ids)))) + (t/is (seq source-texts)))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index b3abbd48d6..59e8c1aa18 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -83,6 +83,7 @@ [common-tests.logic.multiple-nesting-levels-test] [common-tests.logic.override-scan-test] [common-tests.logic.placement-test] + [common-tests.logic.source-gen-test] [common-tests.logic.swap-and-reset-test] [common-tests.logic.swap-as-override-test] [common-tests.logic.swap-group-test] @@ -215,6 +216,7 @@ 'common-tests.logic.multiple-nesting-levels-test 'common-tests.logic.override-scan-test 'common-tests.logic.placement-test + 'common-tests.logic.source-gen-test 'common-tests.logic.swap-and-reset-test 'common-tests.logic.swap-as-override-test 'common-tests.logic.swap-group-test diff --git a/frontend/test/frontend_tests/composable_tests/completion_contract_test.cljs b/frontend/test/frontend_tests/composable_tests/completion_contract_test.cljs index ddec2fd30f..fe4097fd7f 100644 --- a/frontend/test/frontend_tests/composable_tests/completion_contract_test.cljs +++ b/frontend/test/frontend_tests/composable_tests/completion_contract_test.cljs @@ -25,6 +25,7 @@ [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.normalize :as thn] [app.common.test-helpers.shapes :as ths] [app.common.test-helpers.texts :as thtx] [app.common.time :as ct] @@ -35,6 +36,7 @@ [app.main.data.workspace.thumbnails-wasm :as dwthw] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.wasm-text :as dwwt] + [app.main.repo :as rp] [app.main.store :as st] [app.render-wasm.api :as wasm] [app.render-wasm.api.fonts :as wasm.fonts] @@ -395,6 +397,75 @@ (t/is (= red (fill (:id first-copy)))) (t/is (= setup/labeled-fill (fill (:id other-copy)))))) +(defn- ^:async source-acceptance-history + [tuple transform-tuple] + (let [initial (cg/initial-state [2 1 {:library true :stale-libraries true :library-text true}]) + situation (:situation initial) + owner (:library-id (cg/resolve-op situation {} tuple)) + transform-tuple (when transform-tuple + (assoc transform-tuple 1 + (first (keep-indexed + (fn [index entry] + (when (= (thi/id :library-text-component) + (get-in entry [:shape :component-id])) index)) + (cg/candidates situation {} :transform 4))))) + edited (-> initial (cg/step tuple) cg/settle-state) + group (peek (get-in edited [:situation :library-histories owner :runner ::r/undo-stack])) + changes (mapcat :redo-changes group) + ;; The notification envelope advances the loaded library revision. + edited (update edited :situation + #(tm/with-aux-files % (update (tm/aux-files %) owner assoc :revn 1))) + _ (ftm/start-generated situation) + current-id (:id (tm/file situation)) + before (get-in (ftm/snapshot) [:files current-id])] + (t/is (seq changes) (str "source edit changes " tuple)) + (t/is (= :settled (await (ftm/await-step [(dwl/ext-library-changed owner (ct/now) 1 changes)])))) + (t/is (= before (get-in (ftm/snapshot) [:files current-id])) "notification keeps consumer copies stale") + (t/is (empty? (get-in @st/state [:workspace-undo :items])) "notification creates no consumer history") + (when transform-tuple + (let [before-transform (ftm/snapshot) + resolved (cg/resolve-op situation {} transform-tuple) + content (fn [] + (into {} (map (fn [{:keys [shape]}] + [(:id shape) (select-keys shape [:content :name :fills :strokes :shapes :opacity])])) + (cg/tree (ftm/refresh-generated situation)))) + before-content (content)] + (t/is (= :settled (await (ftm/await-step (ftm/op->events (:op resolved) situation))))) + (t/is (= before-content (content)) "a transform keeps pending source content and structure stale") + (let [after-transform (ftm/snapshot)] + (t/is (= :settled (await (ftm/await-step [dwu/undo])))) + (t/is (uc/same-state? before-transform (ftm/snapshot))) + (t/is (= :settled (await (ftm/await-step [dwu/redo])))) + (t/is (uc/same-state? after-transform (ftm/snapshot)))))) + (let [edited (cond-> edited transform-tuple (cg/step transform-tuple)) + before-accept (ftm/snapshot) + expected (-> edited (cg/step [:library-sync]) cg/settle-state)] + (t/is (= :settled (await (ftm/await-step [(dwl/sync-file current-id owner)])))) + (let [actual (ftm/refresh-generated (:situation expected)) + after-accept (ftm/snapshot) + expected-design (oracle/canonical (:situation expected)) + actual-design (oracle/canonical actual)] + (t/is (true? (oracle/equivalent? expected-design actual-design)) + (str "consumer acceptance " tuple ": " (pr-str (thn/diff expected-design actual-design)))) + (t/is (nil? (oracle/check actual (:model expected)))) + (t/is (= :settled (await (ftm/await-step [dwu/undo])))) + (t/is (uc/same-state? before-accept (ftm/snapshot)) "undo restores the stale consumer only") + (t/is (= :settled (await (ftm/await-step [dwu/redo])))) + (t/is (uc/same-state? after-accept (ftm/snapshot)) "redo restores accepted geometry and text"))))) + +(t/deftest ^:async source-geometry-and-text-wait-for-consumer-acceptance + (await + (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock} + (doseq [tuple (concat (map #(vector :library-transform 0 % 4 1) (range 6)) + [[:library-text 0 0 1]] + (map #(vector :library-text-style 0 % 1) (range 12)))] + (await (source-acceptance-history tuple nil)))))) + +(t/deftest ^:async a-transform-keeps-library-text-stale-until-acceptance + (await + (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock} + (await (source-acceptance-history [:library-text 0 1 1] [:transform 8 3 3 4]))))) + (t/deftest ^:async edit-step-waits-for-slow-sync (let [sync-file dwl/sync-file] (await