mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🐛 Keep pending library edits out of transform settlement
Use the component sync result only as a geometry proposal when settling reflections and swapped placement. Correct existing shapes without pulling pending library content, styles or structure into a transformed copy. Keep path geometry and the primary action's exact undo/redo group. Generate source transforms, letters and all twelve text style units with separate source histories and explicit consumer acceptance. Add dropped source-edit controls and portable/frontend regressions for the accidental pull. Register the new portable namespace in the JavaScript runner. The complete original program and both reductions pass unchanged. Common JVM passes 1,919 tests/31,313 assertions. Full JS passes 1,845/29,770; the newly registered source namespace adds 5/135 passing checks. Frontend passes 1,566/11,119, backend 1,610/8,104 and exporter 14/32. Lint and formatting pass; browser ShapeProxy replay passes. No golden data changes. Benchmark ratios against the unchanged baseline: mains 10/100/1000 0.57/0.65/0.61, levels 0.52, chain 0.51, library 0.52, many shapes 0.47, variant switch 0.60. No regression flag. AI-assisted-by: gpt-6
This commit is contained in:
parent
188dac5ad3
commit
2e542dc8cf
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}))
|
||||
|
||||
93
common/test/common_tests/logic/source_gen_test.cljc
Normal file
93
common/test/common_tests/logic/source_gen_test.cljc
Normal file
@ -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))))
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user