mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
✨ Test the component sync paths no test reached
A coverage run found sync paths that no test ran. New cases, in both runners: - AK: update main into a grid main. With a swapped child the main's cells keep the copy child's id (F49, pinned). - AL: update main after renaming the copy root and its child. - AM: update main of a swap inside a group. The copy child keeps a swap slot that names a removed main child (F16, pinned). - AN: a copy flex child's margin override through a main edit, a reset and update main. The frontend round trip of the main edit shows F43 with real sizes (pinned). - AO: a copy dropped into a grid cell of a main. Its undo shows F41 (pinned). - AP: a library main deletes a middle child. - AQ: a main change reaches a copy nested in a sub-board. Also: the frontend interpreter sends the real reset event instead of installing a file; a test undoes and redoes an undo entry without a group, which only release builds store; a test confirms F50, a copy text touched only in :content-group losing its override (pinned).
This commit is contained in:
parent
d97cbac3a3
commit
0868046270
@ -99,8 +99,8 @@ Two pure runners (no store, JVM + CLJS):
|
||||
situation's files) once settled: after the sync fixpoint and after an undo group; never
|
||||
between cascade passes, where an outer copy still points at the component its main dropped
|
||||
(`:component-id-mismatch`), nor with `{:sync? false}`. Assembly ops (create/instantiate/nest/variant
|
||||
container): no sync, no undo group. `n/reset` (`reset-copy-instance` resets a lineage's latest copy) is a user op here (sync +
|
||||
undo), unlike the interpreter (file install). `{:sync? false}` disables sync. `Rotate`/`ChangeHeight`/`MoveShape`
|
||||
container): no sync, no undo group. `n/reset` (`reset-copy-instance` resets a lineage's latest copy) is a user op (sync +
|
||||
undo); the interpreter sends `dwl/reset-component`. `{:sync? false}` disables sync. `Rotate`/`ChangeHeight`/`MoveShape`
|
||||
are `:frontend-only` and throw `::runner/frontend-only` (the placement step is frontend code). Swap and switch nodes use the production
|
||||
`clv/generate-component-swap-in-place` (+ `clv/find-switch-target` for switch), the same code the
|
||||
frontend events call. Not modelled: the `:modified-at` bump, and the swap/switch event follow-ups
|
||||
@ -126,7 +126,7 @@ Two pure runners (no store, JVM + CLJS):
|
||||
changes only when the failure is understood, and the mark goes only in the fix commit. The interpreter runs the same round trip on every `:user` and
|
||||
`:frontend-only` event step through the real `dwu/undo`/`dwu/redo`, and also checks the
|
||||
workspace undo index (`:undo-index`/`:redo-index` phases): a step must add exactly one undo
|
||||
entry or group. File installs (assembly ops, reset) are its baselines; a skipped optional and an inline `Test` are not. The interpreter
|
||||
entry or group. File installs (assembly ops) are its baselines; a skipped optional and an inline `Test` are not. The interpreter
|
||||
empties `:workspace-undo` per variant. Frontend snapshots drop `:position-data` under WASM.
|
||||
Undo groups get lost in derived work: `initialize-shape-layout` buffers `:layout/update`
|
||||
without its `:undo-group` (F43), so a swap's reflow lands in entries of its own. Match
|
||||
@ -145,8 +145,9 @@ Two pure runners (no store, JVM + CLJS):
|
||||
their undo group (F43) at timing-dependent points (`initialize-shape-layout` buffers
|
||||
`:layout/update` in 100 ms windows): undo pins over such reflows are nondeterministic.
|
||||
New cases that are not about geometry pass `:size` (nesting sizes the outer frames too).
|
||||
F43 lives in case AJ only (frontend only, round trip off): it pins the value one undo
|
||||
leaves, which does not depend on how many entries the reflow makes.
|
||||
F43 from float noise lives in case AJ only (frontend only, round trip off): it pins the
|
||||
value one undo leaves, which does not depend on how many entries the reflow makes. Case AN
|
||||
pins F43 from a real reflow (a flex main's margin edit, synced): stable in repeated runs.
|
||||
- Case assertions a known bug breaks use `tm/known-failure bug expected pinned actual`: a
|
||||
strict expected failure at assertion level (passes only while `actual` is the pinned wrong
|
||||
value). The undo-check marks cover only round-trip failures.
|
||||
@ -158,8 +159,7 @@ Two pure runners (no store, JVM + CLJS):
|
||||
- Depth sweeps: `depth-sweep` in `cases.cljc`, a one-of over skip,
|
||||
`(n/make-nested-component name)` and `(n/make-nested-component name 2)`; asserters read
|
||||
`n/nesting-depth`. Two independent nesting optionals run depth 1 twice.
|
||||
- General target-based nodes: `n/swap-shape target to` (`to` a lineage name or a component label), `n/move-to-cell board child row column`, `n/reset target` (a file install
|
||||
in the interpreter). `n/swap-component name level to` is `swap-shape` on `n/nested-head-of`. `n/nested-head-in name` (and the general `n/nested-head-under parent-target`) finds a copy's nested
|
||||
- General target-based nodes: `n/swap-shape target to` (`to` a lineage name or a component label), `n/move-to-cell board child row column`, `n/reset target` (`dwl/reset-component` in the interpreter). `n/swap-component name level to` is `swap-shape` on `n/nested-head-of`. `n/nested-head-in name` (and the general `n/nested-head-under parent-target`) finds a copy's nested
|
||||
head through its parent, so it survives swaps and switches (they keep the head id anyway).
|
||||
Careful: `n/lineage-component-id` is the OUTER component after nesting.
|
||||
- `n/undo-before-sync` (frontend-only) emits an edit and `dwu/undo` in one step, so the undo
|
||||
@ -168,7 +168,7 @@ Two pure runners (no store, JVM + CLJS):
|
||||
before running the next one (`thi/id` then points at the new run's shapes). Case letters B..Z, then AA..;
|
||||
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
|
||||
geometry, on the #10109 fix branch until merged; O: switch of an overridden copy, mains
|
||||
agreeing or not; Q: detach at depth 0..2, nested copies stay linked; R: update-main at depth 0..2 with two copies; S: delete/restore with a copy on another page; T: duplicate a copy at depth 0..2; U: move a copy into a main with a copy; V: new edit after undo empties redo; W: `(n/undo 3)` over nested cascades; X: undo before the edit's sync, frontend only; AJ: F43, one undo after a nested sync reverts only the reflow, frontend only; Y: text override × main text change, per subgroup; Z: main child move, frontend only; AA..AE: swap outcomes asserted on values, not slot storage, so they survive the Phase 2b swap model; AF: grid cell move vs a swapped copy child, setup `grid-component-with-copy`; AG/AH/AI: ports of the plugin's slot-alignment cases, setup `component-with-sibling-instances`, check `setup/misaligned-sub-heads`) are the flagship pattern — read them before
|
||||
agreeing or not; Q: detach at depth 0..2, nested copies stay linked; R: update-main at depth 0..2 with two copies; S: delete/restore with a copy on another page; T: duplicate a copy at depth 0..2; U: move a copy into a main with a copy; V: new edit after undo empties redo; W: `(n/undo 3)` over nested cascades; X: undo before the edit's sync, frontend only; AJ: F43, one undo after a nested sync reverts only the reflow, frontend only; Y: text override × main text change, per subgroup; Z: main child move, frontend only; AA..AE: swap outcomes asserted on values, not slot storage, so they survive the Phase 2b swap model; AF: grid cell move vs a swapped copy child, setup `grid-component-with-copy`; AG/AH/AI: ports of the plugin's slot-alignment cases, setup `component-with-sibling-instances`, check `setup/misaligned-sub-heads`; AK: update main into a grid main, F49 pinned; AL: update main renames the component (`change-property` takes `:name`); AM: update main of a swap inside a group, setup `grouped-component-with-copy`, F16 pinned; AN: flex child margin override through main edit, reset and update main, setup `flex-component-with-copy`; `change-property` takes `:margin`, which the interpreter sends as `dwsl/update-layout-child`; AO: a copy dropped into a grid cell of a main (`nest-copy` with `:cell`, sent as `dwt/move-shapes-to-frame`), F41 pinned; AP: a library main's middle child removal reaches a copy in another file (in one file, deleting a main child deletes the copies' children itself, so sync never meets the copy child), setup `cross-file-component-with-many-children`; AQ: main change reaches a copy nested in a sub-board, setup `component-nested-in-a-sub-board`) are the flagship pattern — read them before
|
||||
writing a new sweep.
|
||||
|
||||
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
|
||||
|
||||
@ -48,6 +48,13 @@
|
||||
AG — a main reorder keeps the copy's nested copies aligned.
|
||||
AH, AI — a copy deleting a nested copy keeps the others aligned (AI also
|
||||
reflows, frontend only).
|
||||
AK — update main pushes a copy's grid cells to a grid main.
|
||||
AL — update main renames the component after a copy root rename.
|
||||
AM — update main pushes a swap made inside a group.
|
||||
AN — a flex child's margin override through sync, reset and update main.
|
||||
AO — a copy dropped into a grid cell of a main reaches the main's copies.
|
||||
AP — a library main's middle child removal reaches a copy in another file.
|
||||
AQ — a main change reaches a copy nested in a sub-board of the outer main.
|
||||
N — main edits reach rotated copies (frontend only).
|
||||
Rotation — a touched copy child survives a main rotation (frontend only too)."
|
||||
(:require
|
||||
@ -62,6 +69,7 @@
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[clojure.test :as t]))
|
||||
@ -876,6 +884,220 @@
|
||||
:asserter (fn [s]
|
||||
(t/is (= [] (setup/misaligned-sub-heads s))))})
|
||||
|
||||
(defn- grid-placement
|
||||
"The cells of grid board `board`, as `{column [child-shape ...]}`."
|
||||
[s board]
|
||||
(let [objects (:objects (thf/current-page (tm/file s)))]
|
||||
(into {}
|
||||
(map (fn [cell] [(:column cell) (mapv #(get objects %) (:shapes cell))]))
|
||||
(vals (:layout-grid-cells board)))))
|
||||
|
||||
(defn update-main-pushes-grid-cells
|
||||
"Case AK. A component's main is a grid board holding two nested copies;
|
||||
optionally its copy swaps the first of them. The copy moves that child to
|
||||
the free third column, then pushes its overrides to the main (update
|
||||
main). The main's child at that position moves to the third column: an
|
||||
instance of the component the copy swapped to, if it swapped, else the
|
||||
original. Every cell of the main holds only children of the main, and each
|
||||
child is in a cell. The copy is left untouched."
|
||||
[]
|
||||
(let [swap (n/swap-shape :copy-child :other)]
|
||||
{:setup setup/grid-component-with-copy
|
||||
:operation (tm/in-sequence [(tm/optional swap)
|
||||
(n/move-to-cell :copy-root :copy-child 1 3)
|
||||
(n/update-main :copy-root)])
|
||||
:asserter (fn [s]
|
||||
(let [main (tm/role-shape s :main-root)
|
||||
copy-child (tm/role-shape s :copy-child)
|
||||
placement (grid-placement s main)
|
||||
in-cells (into #{} (comp (mapcat val) (map :id)) placement)
|
||||
cells {:stray (into #{} (remove (set (:shapes main))) in-cells)
|
||||
:uncelled (count (remove in-cells (:shapes main)))}
|
||||
;; the main's third column takes the copy child's component
|
||||
in-col-3 (mapv :component-id (get placement 3))
|
||||
expected [(:component-id copy-child)]]
|
||||
(if (tm/applied? s swap)
|
||||
;; the cells keep the copy child's id, which no main child has
|
||||
(tm/known-failure "F49"
|
||||
[{:stray #{} :uncelled 0} expected]
|
||||
[{:stray #{(:id copy-child)} :uncelled 1} expected]
|
||||
[cells in-col-3])
|
||||
(do
|
||||
(t/is (= {:stray #{} :uncelled 0} cells))
|
||||
(t/is (= expected in-col-3))))
|
||||
(t/is (empty? (:touched (tm/role-shape s :copy-root))))))}))
|
||||
|
||||
(defn update-main-renames-the-component
|
||||
"Case AL. A copy renames its root and optionally its child, then pushes its
|
||||
overrides to the main (update main). The component and its main root take
|
||||
the root's new name, the main child takes the child's new name when it was
|
||||
renamed, and the copy is left untouched."
|
||||
[]
|
||||
(let [rename-root (n/change-property :copy-root :name "Renamed")
|
||||
rename-child (n/change-property :copy-instance :name "Renamed child")]
|
||||
{:setup setup/simple-component-with-copy
|
||||
:operation (tm/in-sequence [rename-root
|
||||
(tm/optional rename-child)
|
||||
(n/update-main :copy-root)])
|
||||
:asserter (fn [s]
|
||||
(let [main-root (tm/role-shape s :main-root)
|
||||
component (ctkl/get-component (:data (tm/file s))
|
||||
(:component-id main-root))]
|
||||
(t/is (= "Renamed" (:name component)))
|
||||
(t/is (n/has-property-of rename-root main-root))
|
||||
(when (tm/applied? s rename-child)
|
||||
(t/is (n/has-property-of rename-child (tm/role-shape s :main-instance))))
|
||||
(t/is (empty? (:touched (tm/role-shape s :copy-root))))
|
||||
(t/is (empty? (:touched (tm/role-shape s :copy-instance))))))}))
|
||||
|
||||
(defn update-main-pushes-a-swap-inside-a-group
|
||||
"Case AM. A component's main holds a nested copy inside a group; its copy
|
||||
swaps that nested copy for another component, then pushes its overrides to
|
||||
the main (update main). The main's group holds an instance of the
|
||||
component swapped to in place of the old one, the copy's swapped child
|
||||
references it, and the copy is left untouched."
|
||||
[]
|
||||
{:setup setup/grouped-component-with-copy
|
||||
:operation (tm/in-sequence [(n/swap-shape :copy-child :other)
|
||||
(n/update-main :copy-root)])
|
||||
:asserter (fn [s]
|
||||
(let [objects (:objects (thf/current-page (tm/file s)))
|
||||
copy-child (tm/role-shape s :copy-child)
|
||||
group (get objects (:parent-id (get objects (:shape-ref copy-child))))
|
||||
in-group (mapv #(get objects %) (:shapes group))]
|
||||
(t/is (= (:id (tm/role-shape s :main-root)) (:parent-id group)))
|
||||
(t/is (= [(:component-id copy-child)] (mapv :component-id in-group)))
|
||||
(t/is (= [(:shape-ref copy-child)] (mapv :id in-group)))
|
||||
(t/is (empty? (:touched (tm/role-shape s :copy-root))))
|
||||
;; update main leaves the swapped child its swap slot
|
||||
(tm/known-failure "F16" #{} #{:swap-slot}
|
||||
(into #{}
|
||||
(map #(if (ctk/swap-slot? %) :swap-slot %))
|
||||
(:touched copy-child)))))})
|
||||
|
||||
(defn flex-child-override-follows-sync-reset-and-update-main
|
||||
"Case AN. A copy of a flex main overrides its child's margin. Then the main
|
||||
changes that margin, the copy resets, or the copy runs update main. After
|
||||
the main's change the copy keeps its own margin, touched; after the reset
|
||||
it shows the main's, untouched; after update main the main shows the
|
||||
copy's and the copy is untouched."
|
||||
[]
|
||||
(let [override (n/change-property :copy-instance :margin 8)
|
||||
main-change (n/change-property :main-instance :margin 20)
|
||||
reset (n/reset :copy-root)
|
||||
update-main (n/update-main :copy-root)
|
||||
follow-up (tm/one-of [main-change reset update-main])]
|
||||
{:setup setup/flex-component-with-copy
|
||||
;; the reflow after the sync of the main's margin lands in undo entries
|
||||
;; of its own, so one undo leaves the margin and the reflow in place
|
||||
:undo-check {:known-failures [{:bug "F43"
|
||||
:fails {:undo #{[:object :main-child :layout-item-margin]
|
||||
[:object :main-child :layout-item-margin-type]
|
||||
[:object :main-child :x]
|
||||
[:object :main-child :selrect]
|
||||
[:object :main-child :points]}
|
||||
:undo-index #{[:undo-index 2]}}
|
||||
:op main-change
|
||||
:runners #{:frontend}}]}
|
||||
:operation (tm/in-sequence [override follow-up])
|
||||
:asserter (fn [s]
|
||||
(let [copy (tm/role-shape s :copy-instance)
|
||||
main (tm/role-shape s :main-instance)
|
||||
margin (fn [shape] (-> shape :layout-item-margin :m1))
|
||||
chosen (tm/get-choice s follow-up)
|
||||
touched (contains? (:touched copy) :layout-item-margin)]
|
||||
(condp = chosen
|
||||
main-change (do (t/is (n/has-property-of override copy))
|
||||
(t/is (n/has-property-of main-change main))
|
||||
(t/is touched))
|
||||
reset (do (t/is (= (margin main) (margin copy)))
|
||||
(t/is (not (n/has-property-of override copy)))
|
||||
(t/is (not touched)))
|
||||
update-main (do (t/is (n/has-property-of override main))
|
||||
(t/is (n/has-property-of override copy))
|
||||
(t/is (not touched))))))}))
|
||||
|
||||
(defn copy-dropped-into-a-grid-cell-reaches-copies
|
||||
"Case AO. A grid main holds two nested copies in its first two columns, the
|
||||
third free; a loose copy of the same component is dropped into the third
|
||||
cell of the main. The main holds it in that cell, and the main's copy gets
|
||||
a child that references it, in its own third cell. Every child of both is
|
||||
in a cell, and the copy is left untouched."
|
||||
[]
|
||||
(let [drop (n/nest-copy :loose :main-root :cell [1 3])]
|
||||
{:setup setup/grid-component-with-copy-and-loose-item
|
||||
;; the drop reorders the main's children; the undo of the sync's reorder
|
||||
;; leaves the copy's children reordered
|
||||
:undo-check {:known-failures [{:bug "F41"
|
||||
:fails {:undo #{[:object :grid-copy :shapes]}
|
||||
:variant-undo #{[:object :grid-copy :shapes]}}
|
||||
:op drop}]}
|
||||
:operation (tm/in-sequence [drop])
|
||||
:asserter (fn [s]
|
||||
(let [main (tm/role-shape s :main-root)
|
||||
copy (tm/role-shape s :copy-root)
|
||||
loose-id (:id (tm/role-shape s :loose))
|
||||
placement (fn [board]
|
||||
(let [placed (grid-placement s board)]
|
||||
{:col-3 (mapv :id (get placed 3))
|
||||
:uncelled (count (remove (into #{} (comp (mapcat val) (map :id)) placed)
|
||||
(:shapes board)))}))
|
||||
in-copy (d/seek #(= loose-id (:shape-ref %))
|
||||
(map #(tm/shape-by-id s %) (:shapes copy)))]
|
||||
(t/is (= {:col-3 [loose-id] :uncelled 0} (placement main)))
|
||||
(t/is (some? in-copy))
|
||||
(t/is (= {:col-3 [(:id in-copy)] :uncelled 0} (placement copy)))
|
||||
(t/is (empty? (:touched copy)))
|
||||
(t/is (empty? (:touched in-copy)))))}))
|
||||
|
||||
(defn library-main-child-removal-reaches-copy
|
||||
"Case AP. A library main with three children deletes its middle one; a copy
|
||||
in the consuming file then takes the library update. The copy loses its
|
||||
middle child and keeps the others in order, linked and untouched."
|
||||
[]
|
||||
{:setup setup/cross-file-component-with-many-children
|
||||
:operation (tm/in-sequence [(n/sync-from-library)])
|
||||
:asserter (fn [s]
|
||||
(let [c1 (tm/role-shape s :copy-child-1)
|
||||
c3 (tm/role-shape s :copy-child-3)]
|
||||
(t/is (= [(:id c1) (:id c3)] (:shapes (tm/role-shape s :copy-root))))
|
||||
(t/is (some? (:shape-ref c1)))
|
||||
(t/is (some? (:shape-ref c3)))
|
||||
(t/is (empty? (:touched c1)))
|
||||
(t/is (empty? (:touched c3)))))})
|
||||
|
||||
(defn main-change-reaches-a-copy-nested-in-a-sub-board
|
||||
"Case AQ. An outer main holds a copy of an inner component inside a
|
||||
sub-board, not directly under its root; the outer main has a copy, which
|
||||
optionally overrides the inner rect's fill. The inner main changes that
|
||||
fill. The outer main's nested copy shows the change, and so does the outer
|
||||
copy unless it overrode it: then it keeps its value, touched."
|
||||
[]
|
||||
(let [override (n/change-property (fn [s]
|
||||
(n/corresponding-shape-id s
|
||||
(:id (tm/role-shape s :copy-root))
|
||||
(:id (tm/role-shape s :main-instance))))
|
||||
:fills green)
|
||||
change (n/change-property :main-instance :fills red)]
|
||||
{:setup setup/component-nested-in-a-sub-board
|
||||
:operation (tm/in-sequence [(tm/optional override) change])
|
||||
:asserter (fn [s]
|
||||
(let [image (fn [role]
|
||||
(tm/shape-by-id s (n/corresponding-shape-id
|
||||
s
|
||||
(:id (tm/role-shape s role))
|
||||
(:id (tm/role-shape s :main-instance)))))
|
||||
in-main (image :main-root)
|
||||
in-copy (image :copy-root)]
|
||||
(t/is (n/has-property-of change in-main))
|
||||
(if (tm/applied? s override)
|
||||
(do
|
||||
(t/is (n/has-property-of override in-copy))
|
||||
(t/is (contains? (:touched in-copy) :fill-group)))
|
||||
(do
|
||||
(t/is (n/has-property-of change in-copy))
|
||||
(t/is (empty? (:touched in-copy)))))))}))
|
||||
|
||||
(defn geometry-sync-with-rotated-instances
|
||||
"Case N (frontend only). Optionally the copy rotates, optionally the main
|
||||
rotates, then the main child changes its fill or its height. The change
|
||||
|
||||
@ -80,8 +80,12 @@
|
||||
(case property
|
||||
:fills (assoc shape :fills (ths/sample-fills-color :fill-color value))
|
||||
:opacity (assoc shape :opacity value)
|
||||
:name (assoc shape :name value)
|
||||
:margin (assoc shape
|
||||
:layout-item-margin-type :simple
|
||||
:layout-item-margin {:m1 value :m2 value :m3 value :m4 value})
|
||||
(throw (ex-info (str "set-property: unsupported property " (pr-str property)
|
||||
" (supported: :fills, :opacity)")
|
||||
" (supported: :fills, :opacity, :name, :margin)")
|
||||
{:property property}))))
|
||||
|
||||
(defn- read-property
|
||||
@ -90,8 +94,10 @@
|
||||
(case property
|
||||
:fills (-> shape :fills first :fill-color)
|
||||
:opacity (:opacity shape)
|
||||
:name (:name shape)
|
||||
:margin (-> shape :layout-item-margin :m1)
|
||||
(throw (ex-info (str "read-property: unsupported property " (pr-str property)
|
||||
" (supported: :fills, :opacity)")
|
||||
" (supported: :fills, :opacity, :name, :margin)")
|
||||
{:property property}))))
|
||||
|
||||
(defrecord ChangeProperty [target property value]
|
||||
@ -687,9 +693,8 @@
|
||||
;; reset — reset the overrides of a copy head
|
||||
;;
|
||||
;; The "Reset overrides" action on the copy head bound to `target`, a nested
|
||||
;; head included, through the production `generate-reset-component`. The
|
||||
;; frontend interpreter runs it as a file install: the real reset event reads
|
||||
;; browser globals, so it cannot run headless.
|
||||
;; head included, through the production `generate-reset-component`
|
||||
;; (`reset-component` on the frontend).
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defrecord ResetOverrides [target]
|
||||
@ -955,7 +960,8 @@
|
||||
;;
|
||||
;; Moves the copy head bound to `copy` under the shape bound to `into`, as its
|
||||
;; last child, through `generate-relocate`, as dragging it in the layers
|
||||
;; panel does. Unlike `make-nested-component` it is a user action, so the
|
||||
;; panel does; with a `cell` ([row column] of a grid `into`), into that cell,
|
||||
;; as a canvas drop does. Unlike `make-nested-component` it is a user action, so the
|
||||
;; receiving main's copies sync and the step goes through undo.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@ -965,7 +971,7 @@
|
||||
[situation parent-id]
|
||||
(count (:shapes (tm/shape-by-id situation parent-id))))
|
||||
|
||||
(defrecord NestCopy [copy into]
|
||||
(defrecord NestCopy [copy into cell]
|
||||
IComponentOperation
|
||||
(op-kind [_] :user)
|
||||
|
||||
@ -980,18 +986,20 @@
|
||||
(pcb/with-objects (:objects page)))
|
||||
parent-id
|
||||
(nest-target-index situation parent-id)
|
||||
#{copy-id})
|
||||
#{copy-id}
|
||||
:cell cell)
|
||||
file' (thf/apply-changes the-file changes :validate? false)]
|
||||
(-> situation
|
||||
(tm/with-file file')
|
||||
(record-changes changes)
|
||||
(tm/record-application this {:copy copy :into into})))))
|
||||
(tm/record-application this {:copy copy :into into :cell cell})))))
|
||||
|
||||
(defn nest-copy
|
||||
"Constructor for the nest-copy operation: move the copy head `copy` names
|
||||
under the shape `into` names, as its last child."
|
||||
[copy into]
|
||||
(tm/assign-id (->NestCopy copy into)))
|
||||
under the shape `into` names, as its last child, or into the grid cell
|
||||
`cell` ([row column]) when given."
|
||||
[copy into & {:keys [cell]}]
|
||||
(tm/assign-id (->NestCopy copy into cell)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; swap-shape — swap a copy head for another component
|
||||
|
||||
@ -39,6 +39,22 @@
|
||||
:copy-root :copy-root})))
|
||||
|
||||
|
||||
(defn flex-component-with-copy
|
||||
"Like `simple-component-with-copy`, with a 100x100 main root that has a
|
||||
flex layout (row) and a 50x50 child. Same labels and roles."
|
||||
[]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-simple-component-with-copy
|
||||
:component1 :main-root :main-child :copy-root
|
||||
:main-root-params {:layout :flex :layout-flex-dir :row
|
||||
:width 100 :height 100}
|
||||
:main-child-params {:width 50 :height 50}
|
||||
:copy-root-params {:children-labels [:copy-child]}))]
|
||||
(tm/make-situation file {:main-instance :main-child
|
||||
:copy-instance :copy-child
|
||||
:main-root :main-root
|
||||
:copy-root :copy-root})))
|
||||
|
||||
(defn empty-situation
|
||||
"A situation with an empty file and no roles, for cases that build their
|
||||
configuration with operations (`create-component` and the like)."
|
||||
@ -145,6 +161,43 @@
|
||||
:main-child :child1
|
||||
:copy-child :copy-child1})))
|
||||
|
||||
(defn grouped-component-with-copy
|
||||
"A component whose main board holds a group with a copy of an `:item`
|
||||
component, and a copy of it. A second component, `:other`, has a red rect
|
||||
(a swap target). Labels: :grouped-root :group :child1 (main side),
|
||||
:grouped-copy :copy-group :copy-child1 (copy side), :item, :other. Roles:
|
||||
:main-root, :copy-root, :main-child -> :child1, :copy-child -> :copy-child1."
|
||||
[]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-simple-component :item :item-root :item-rect
|
||||
:root-params {:width 100 :height 100}
|
||||
:child-params {:width 50 :height 50})
|
||||
(tho/add-simple-component :other :other-root :other-rect
|
||||
:root-params {:width 100 :height 100}
|
||||
:child-params {:width 50 :height 50
|
||||
:fills (ths/sample-fills-color
|
||||
:fill-color "#ff0000")})
|
||||
(tho/add-frame :grouped-root :width 200 :height 200)
|
||||
(tho/add-group :group :parent-label :grouped-root)
|
||||
(thc/instantiate-component :item :child1 :parent-label :group)
|
||||
(thc/make-component :grouped :grouped-root)
|
||||
(thc/instantiate-component :grouped :grouped-copy))]
|
||||
(label-copy-children! file :grouped-copy {:group :copy-group})
|
||||
(label-copy-children! file :copy-group {:child1 :copy-child1})
|
||||
(tm/make-situation file {:main-root :grouped-root
|
||||
:copy-root :grouped-copy
|
||||
:main-child :child1
|
||||
:copy-child :copy-child1})))
|
||||
|
||||
(defn grid-component-with-copy-and-loose-item
|
||||
"`grid-component-with-copy` plus a top-level copy of `:item`, labelled
|
||||
:loose. Same roles, plus :loose."
|
||||
[]
|
||||
(let [situation (grid-component-with-copy)]
|
||||
(-> situation
|
||||
(tm/with-file (thc/instantiate-component (tm/file situation) :item :loose))
|
||||
(assoc-in [:roles :loose] (thi/id :loose)))))
|
||||
|
||||
(defn component-with-sibling-instances
|
||||
"A component whose main (300x100) holds three copies of an `:icon`
|
||||
component side by side, and a copy of it. With `:grid` layout the main is a
|
||||
@ -279,3 +332,55 @@
|
||||
(-> (tm/make-situation file {:copy-instance :copy-child
|
||||
:copy-root :copy-root})
|
||||
(tm/with-aux-files {(:id library') library'}))))
|
||||
|
||||
(defn cross-file-component-with-many-children
|
||||
"A component with three children in a shared library file and a copy of it
|
||||
in the current file; the library main then deletes its middle child, so
|
||||
the copy is behind it. The library travels as an auxiliary file. Labels:
|
||||
in the library :component1 :main-root :main-child1/2/3, in the file
|
||||
:copy-root :copy-child1/2/3. Roles: :copy-root and :copy-child-1/2/3."
|
||||
[]
|
||||
(let [library (-> (thf/sample-file :library :is-shared true)
|
||||
(tho/add-component-with-many-children
|
||||
:component1 :main-root [:main-child1 :main-child2 :main-child3]))
|
||||
file (-> (thf/sample-file :file)
|
||||
(thc/instantiate-component :component1 :copy-root
|
||||
:library library
|
||||
:children-labels [:copy-child1
|
||||
:copy-child2
|
||||
:copy-child3]))
|
||||
page (thf/current-page library)
|
||||
[_ del] (cls/generate-delete-shapes (pcb/empty-changes nil (:id page))
|
||||
(:data library)
|
||||
page
|
||||
(:objects page)
|
||||
#{(thi/id :main-child2)}
|
||||
{})
|
||||
library' (thf/apply-changes library del)]
|
||||
(-> (tm/make-situation file {:copy-root :copy-root
|
||||
:copy-child-1 :copy-child1
|
||||
:copy-child-2 :copy-child2
|
||||
:copy-child-3 :copy-child3})
|
||||
(tm/with-aux-files {(:id library') library'}))))
|
||||
|
||||
(defn component-nested-in-a-sub-board
|
||||
"An `:inner` component (root and rect) and an `:outer` one whose main board
|
||||
holds a sub-board with a copy of `:inner` in it, and a copy of `:outer`.
|
||||
Labels: :inner-root :inner-rect, :outer-root :sub-board :nested,
|
||||
:outer-copy. Roles: :main-instance -> :inner-rect, :main-root ->
|
||||
:outer-root, :copy-root -> :outer-copy."
|
||||
[]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-simple-component :inner :inner-root :inner-rect
|
||||
:root-params {:width 50 :height 50}
|
||||
:child-params {:width 20 :height 20
|
||||
:fills (ths/sample-fills-color
|
||||
:fill-color labeled-fill)})
|
||||
(tho/add-frame :outer-root :width 200 :height 200)
|
||||
(tho/add-frame :sub-board :parent-label :outer-root :width 100 :height 100)
|
||||
(thc/instantiate-component :inner :nested :parent-label :sub-board)
|
||||
(thc/make-component :outer :outer-root)
|
||||
(thc/instantiate-component :outer :outer-copy))]
|
||||
(tm/make-situation file {:main-instance :inner-rect
|
||||
:main-root :outer-root
|
||||
:copy-root :outer-copy})))
|
||||
|
||||
@ -112,3 +112,24 @@
|
||||
|
||||
(t/deftest case-ah-copy-sub-head-delete-keeps-others-aligned
|
||||
(check (cases/copy-sub-head-delete-keeps-others-aligned)))
|
||||
|
||||
(t/deftest case-ak-update-main-pushes-grid-cells
|
||||
(check (cases/update-main-pushes-grid-cells)))
|
||||
|
||||
(t/deftest case-al-update-main-renames-the-component
|
||||
(check (cases/update-main-renames-the-component)))
|
||||
|
||||
(t/deftest case-am-update-main-pushes-a-swap-inside-a-group
|
||||
(check (cases/update-main-pushes-a-swap-inside-a-group)))
|
||||
|
||||
(t/deftest case-an-flex-child-override-follows-sync-reset-and-update-main
|
||||
(check (cases/flex-child-override-follows-sync-reset-and-update-main)))
|
||||
|
||||
(t/deftest case-ao-copy-dropped-into-a-grid-cell-reaches-copies
|
||||
(check (cases/copy-dropped-into-a-grid-cell-reaches-copies)))
|
||||
|
||||
(t/deftest case-ap-library-main-child-removal-reaches-copy
|
||||
(check (cases/library-main-child-removal-reaches-copy)))
|
||||
|
||||
(t/deftest case-aq-main-change-reaches-a-copy-nested-in-a-sub-board
|
||||
(check (cases/main-change-reaches-a-copy-nested-in-a-sub-board)))
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
@ -879,3 +880,50 @@
|
||||
(t/is (= "14" (:font-size line)))
|
||||
;; The text doesn't change, because the structure was touched
|
||||
(t/is (= "hello world" (:text line)))))
|
||||
|
||||
(t/deftest sync-copy-touched-only-in-content-group
|
||||
;; A copy text whose `:touched` has `:content-group` but no text subgroup,
|
||||
;; as files from before the subgroups hold it, keeps its own content
|
||||
;; when the main changes.
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame-with-text :main-root :main-child "hello world")
|
||||
(thc/make-component :component1 :main-root)
|
||||
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
|
||||
copy-child (ths/get-shape file :copy-child)
|
||||
file (-> file
|
||||
(ths/update-shape :copy-child :content
|
||||
(assoc-in (:content copy-child)
|
||||
[:children 0 :children 0 :children 0 :text]
|
||||
"copy text"))
|
||||
(ths/update-shape :copy-child :touched #{:content-group}))
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id :main-child)}
|
||||
(fn [shape]
|
||||
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
updated-file (thf/apply-changes file changes1)
|
||||
|
||||
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
|
||||
nil
|
||||
:components
|
||||
(:id updated-file)
|
||||
(thi/id :component1)
|
||||
(:id updated-file)
|
||||
{(:id updated-file) updated-file}
|
||||
(:id updated-file))
|
||||
|
||||
file' (thf/apply-changes updated-file changes2)
|
||||
|
||||
;; ==== Get
|
||||
copy-child' (ths/get-shape file' :copy-child)
|
||||
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
|
||||
;; with no subgroup, sync takes the "nothing touched" branch and writes
|
||||
;; the main's content
|
||||
(tm/known-failure "F50" "copy text" "hello world" (:text line))
|
||||
(t/is (= #{:content-group} (:touched copy-child')))))
|
||||
|
||||
@ -116,3 +116,24 @@
|
||||
|
||||
(t/deftest case-ai-copy-sub-head-delete-and-reflow-keeps-others-aligned
|
||||
(t/async done (ftm/check done (cases/copy-sub-head-delete-and-reflow-keeps-others-aligned))))
|
||||
|
||||
(t/deftest case-ak-update-main-pushes-grid-cells
|
||||
(t/async done (ftm/check done (cases/update-main-pushes-grid-cells))))
|
||||
|
||||
(t/deftest case-al-update-main-renames-the-component
|
||||
(t/async done (ftm/check done (cases/update-main-renames-the-component))))
|
||||
|
||||
(t/deftest case-am-update-main-pushes-a-swap-inside-a-group
|
||||
(t/async done (ftm/check done (cases/update-main-pushes-a-swap-inside-a-group))))
|
||||
|
||||
(t/deftest case-an-flex-child-override-follows-sync-reset-and-update-main
|
||||
(t/async done (ftm/check done (cases/flex-child-override-follows-sync-reset-and-update-main))))
|
||||
|
||||
(t/deftest case-ao-copy-dropped-into-a-grid-cell-reaches-copies
|
||||
(t/async done (ftm/check done (cases/copy-dropped-into-a-grid-cell-reaches-copies))))
|
||||
|
||||
(t/deftest case-ap-library-main-child-removal-reaches-copy
|
||||
(t/async done (ftm/check done (cases/library-main-child-removal-reaches-copy))))
|
||||
|
||||
(t/deftest case-aq-main-change-reaches-a-copy-nested-in-a-sub-board
|
||||
(t/async done (ftm/check done (cases/main-change-reaches-a-copy-nested-in-a-sub-board))))
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.reflow :as wrf]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwtx]
|
||||
[app.main.data.workspace.thumbnails :as dwth]
|
||||
@ -89,9 +90,14 @@
|
||||
[op situation]
|
||||
(cond
|
||||
(instance? n/ChangeProperty op)
|
||||
(let [{:keys [target property value]} op]
|
||||
[(dwsh/update-shapes #{(tm/target-shape-id situation target)}
|
||||
(fn [shape] (n/set-property shape property value)))])
|
||||
(let [{:keys [target property value]} op
|
||||
id (tm/target-shape-id situation target)]
|
||||
(if (= :margin property)
|
||||
;; the sidebar's layout child margin
|
||||
(let [shape (n/set-property {} property value)]
|
||||
[(dwsl/update-layout-child [id] (select-keys shape [:layout-item-margin
|
||||
:layout-item-margin-type]))])
|
||||
[(dwsh/update-shapes #{id} (fn [shape] (n/set-property shape property value)))]))
|
||||
|
||||
(instance? n/EditText op)
|
||||
;; the text editor's commit on blur, SVG renderer path
|
||||
@ -146,6 +152,9 @@
|
||||
(instance? n/Detach op)
|
||||
[(dwl/detach-component (tm/target-shape-id situation (:target op)))]
|
||||
|
||||
(instance? n/ResetOverrides op)
|
||||
[(dwl/reset-component (tm/target-shape-id situation (:target op)))]
|
||||
|
||||
(instance? n/UpdateMain op)
|
||||
[(dwl/update-component-sync (tm/target-shape-id situation (:target op))
|
||||
(:id (tm/file situation)))]
|
||||
@ -159,10 +168,13 @@
|
||||
:change-selection? false)]
|
||||
|
||||
(instance? n/NestCopy op)
|
||||
(let [parent-id (tm/target-shape-id situation (:into op))]
|
||||
[(dwsh/relocate-shapes #{(tm/target-shape-id situation (:copy op))}
|
||||
parent-id
|
||||
(n/nest-target-index situation parent-id))])
|
||||
(let [parent-id (tm/target-shape-id situation (:into op))
|
||||
copy-id (tm/target-shape-id situation (:copy op))
|
||||
index (n/nest-target-index situation parent-id)]
|
||||
(if-let [cell (:cell op)]
|
||||
;; the canvas drop into a grid cell
|
||||
[(dwt/move-shapes-to-frame #{copy-id} parent-id index cell)]
|
||||
[(dwsh/relocate-shapes #{copy-id} parent-id index)]))
|
||||
|
||||
(instance? n/RestoreComponent op)
|
||||
[(dwl/restore-component (:id (tm/file situation))
|
||||
@ -423,14 +435,12 @@
|
||||
|
||||
(defn- sync-op?
|
||||
"Whether the op unit `op` runs its `apply-to` on the live file instead of
|
||||
dispatching events: assembly nodes, the reset, `skip` and inline tests."
|
||||
dispatching events: assembly nodes, `skip` and inline tests."
|
||||
[op]
|
||||
(let [op (unwrap op)]
|
||||
(or (instance? tm/Skip op)
|
||||
(instance? tm/Test op)
|
||||
(= :assembly (n/op-kind op))
|
||||
;; a user operation, but the real reset event reads browser globals
|
||||
(instance? n/ResetOverrides op))))
|
||||
(= :assembly (n/op-kind op)))))
|
||||
|
||||
(defn- run-sync-op
|
||||
"Run the `apply-to` of the `sync-op?` op unit `op` on the live file and
|
||||
|
||||
75
frontend/test/frontend_tests/data/workspace_undo_test.cljs
Normal file
75
frontend/test/frontend_tests/data/workspace_undo_test.cljs
Normal file
@ -0,0 +1,75 @@
|
||||
;; 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 frontend-tests.data.workspace-undo-test
|
||||
"Tests undo and redo of an undo entry with no undo group. The entry schema
|
||||
requires a group, but release builds elide the check, so a commit that
|
||||
passes a nil `:undo-group` stores such an entry; undo and redo take a
|
||||
branch of their own for it."
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.composable.comp.setups :as setup]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.store :as st]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.composable-tests.interpreter :as ftm]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each ftm/fixture)
|
||||
|
||||
(def ^:private red "#ff0000")
|
||||
|
||||
(defn- fill-main-child-changes
|
||||
"The changes that fill the main child of the live file `color`."
|
||||
[color]
|
||||
(let [page (thf/current-page (ftm/current-file))]
|
||||
(cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id :main-child)}
|
||||
#(assoc % :fills (ths/sample-fills-color :fill-color color))
|
||||
(:objects page)
|
||||
{})))
|
||||
|
||||
(defn- push-entry-without-group
|
||||
"An event that adds `changes` to the undo stack as an entry with no undo
|
||||
group, as a release build stores a commit whose `:undo-group` is nil."
|
||||
[changes]
|
||||
(ptk/reify ::push-entry-without-group
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-undo
|
||||
(fn [undo]
|
||||
(let [items (conj (vec (:items undo))
|
||||
{:undo-changes (:undo-changes changes)
|
||||
:redo-changes (:redo-changes changes)
|
||||
:tags #{}})]
|
||||
(assoc undo :items items :index (dec (count items)))))))))
|
||||
|
||||
(defn- undo-index
|
||||
[]
|
||||
(get-in @st/state [:workspace-undo :index]))
|
||||
|
||||
(t/deftest ^:async undo-and-redo-an-entry-without-undo-group
|
||||
(ftm/install! (setup/simple-component-with-labeled-copy))
|
||||
(let [changes (fill-main-child-changes red)]
|
||||
(await (ftm/await-step [(dch/commit-changes {:redo-changes (:redo-changes changes)
|
||||
:undo-changes (:undo-changes changes)
|
||||
:save-undo? false})
|
||||
(push-entry-without-group changes)]))
|
||||
(t/is (= red (ftm/fill-of :main-child)))
|
||||
(t/is (= 0 (undo-index)))
|
||||
|
||||
(t/is (= :settled (await (ftm/await-step [dwu/undo]))))
|
||||
(t/is (= setup/labeled-fill (ftm/fill-of :main-child)))
|
||||
(t/is (= -1 (undo-index)))
|
||||
|
||||
(t/is (= :settled (await (ftm/await-step [dwu/redo]))))
|
||||
(t/is (= red (ftm/fill-of :main-child)))
|
||||
(t/is (= 0 (undo-index)))))
|
||||
@ -41,6 +41,7 @@
|
||||
[frontend-tests.data.workspace-sync-barrier-test]
|
||||
[frontend-tests.data.workspace-texts-test]
|
||||
[frontend-tests.data.workspace-thumbnails-test]
|
||||
[frontend-tests.data.workspace-undo-test]
|
||||
[frontend-tests.data.workspace-versions-test]
|
||||
[frontend-tests.debug-logging-test]
|
||||
[frontend-tests.errors-governor-test]
|
||||
@ -200,6 +201,7 @@
|
||||
'frontend-tests.data.workspace-sync-barrier-test
|
||||
'frontend-tests.data.workspace-texts-test
|
||||
'frontend-tests.data.workspace-thumbnails-test
|
||||
'frontend-tests.data.workspace-undo-test
|
||||
'frontend-tests.data.workspace-versions-test
|
||||
'frontend-tests.debug-logging-test
|
||||
'frontend-tests.errors-governor-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user