mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Generate component programs across library files
Build a shared library world and route edits, updates and settlement across working and library files. Check the independent override model, geometry, propagation and exact undo for both files. Replay shrunk library failures through the async frontend with the same target choices and barriers. Keep common generation/shrinking and CLJS replay as the main regression suite. Use the plugin for public API, WASM and measured-text smoke checks; retain its existing fixed cases until equivalent coverage replaces them. All four properties pass 5000 programs each with seed712, each in a full run. Fresh swaps: 5009 assertions; library: 5008. Isolated Stage3: 55/379. Live WASM, fonts, undo/redo and later propagation checks pass. Full frontend: 1539 tests, 9312 assertions, including 39 generated replays. Common JVM: 1851 tests, 30684 assertions; JS: 1782 tests, 29308 assertions. Backend: 1610 tests, 8104 assertions; exporter: 14 tests, 32 assertions. Plugin: 68 cases. Lint, formatting and unchanged goldens pass. AI-assisted-by: GPT-6
This commit is contained in:
parent
3f948c72db
commit
80b3cbfa7a
@ -30,7 +30,7 @@ Masters are not normally touched through `set-shape-attr`, but touched flags can
|
||||
|
||||
- A copy sub-head the user SWAPPED to another component keeps `:shape-ref` on the near-main child it replaces (its *position*); `:component-id`/`:component-file` name the component its *content* comes from; the touched group `:swap-group` marks the swap. Its children refer into its own component's main.
|
||||
- So for a swapped head the position shape (`find-ref-shape`, X in the near main) and the content shape (its component's main root) differ. `ctf/find-ref-shape` gives position (parent heads are tried outermost first); code that needs the content of a swapped head goes through the content helper, never through `get-ref-shape` with the head's own component.
|
||||
- A copy sub-head whose component differs from its near main's component must have `:swap-group`; one that matches must not.
|
||||
- A copy sub-head whose component differs from its near main's component must have `:swap-group`. An authored swap keeps this group if a later main edit chooses the same component: equal does not mean untouched. Only an explicit swap back clears it.
|
||||
- A head takes a position only through `ctf/take-position` (swap in a copy, keep-touched switch, update-main into a copy, the `:ref-not-in-near-main` repair): it refers to the position, is swapped iff its component differs, and otherwise its descendants are remapped to the position's descendants. Setting `:shape-ref`/`:swap-group` by hand leaves the children referring to the wrong main, which validation does not catch and the next sync turns into delete + re-add.
|
||||
- Copy children are paired to main children by `shape-ref` (`is-main-of?`, seek, not index), swapped or not. `find-near-match` (positional) is only a validator/repair heuristic; validity requires membership of the ref among the near-main parent's children, not index equality (`mem:common/file-change-validation-migration-subtleties`).
|
||||
- Copy child ORDER converges to the main's via the async sync (`moved` branch of `compare-children`); local code must never reorder copy children directly (guards in `:mov-objects`/`:reorder-children`).
|
||||
@ -81,3 +81,15 @@ that update-main just pushed (F168). The source copy clears its overrides.
|
||||
Compare against the parent's pushed frame: the inverse sync has already
|
||||
written its new rotation, while the original container still has the old
|
||||
one. Using that old frame creates phantom geometry overrides (F172).
|
||||
|
||||
Pushing a swapped subtree into a nested copy can remap it to an ordinary
|
||||
position with the same component. Content differences against that new
|
||||
reference become authored overrides on the promoted head and descendants;
|
||||
otherwise a later full sync discards the values update-main just pushed
|
||||
(F174). The source copy clears its overrides and follows the new subtree.
|
||||
|
||||
When sync turns a parent, its subtree moves with that turn before the child
|
||||
walk continues (F179). This includes ordinary and swapped children with
|
||||
authored geometry. Normalize parent axis directions for the turn: resize
|
||||
already runs through dimensions and constraints. Refresh the walk from
|
||||
current builder objects so later direct syncs do not turn stale records.
|
||||
|
||||
@ -102,3 +102,106 @@ inside shape geometry: x/y, width/height, rotation, points, selrect and transfor
|
||||
matrices. Reflow can recompute 0.01 as 0.010000000000005116 after undo (F130).
|
||||
This tolerance does not apply to opacity, other authored attrs or touched groups.
|
||||
The regression rejects a 1e-5 height change and a 5e-15 opacity change.
|
||||
|
||||
|
||||
## Generated component programs
|
||||
|
||||
`composable/gen` builds seeded EDN programs and resolves targets from current
|
||||
shape-tree order. Run `common-tests.logic.composable-gen-test`; set
|
||||
`PENPOT_GEN_NUM` and `PENPOT_GEN_SEED` to replay a count and seed. Before merge,
|
||||
Phase 4 requires 5000 scenarios per property. `cg/run` accepts the printed
|
||||
program and an optional expected trace; changed targets or skips fail replay.
|
||||
Use separate fresh JVMs for concurrent checks: `world` resets the global test
|
||||
label map, so concurrent futures in one JVM corrupt each other's fixtures.
|
||||
|
||||
The independent shadow tracks authored groups and values; the oracle checks
|
||||
validation, touched membership, retained values, propagation and geometry.
|
||||
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.
|
||||
|
||||
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
|
||||
run splitting are omitted. Free copy-head placement after leaving a layout is
|
||||
also omitted: pure execution has no reflow history. Each backend still checks
|
||||
child offsets and propagation; a changed child offset fails the comparison.
|
||||
These exclusions do not apply to undo snapshots or authored scalar values.
|
||||
Swapped-head constraints and layout-item inputs use the position reference;
|
||||
content inputs use the component's content main.
|
||||
|
||||
|
||||
For long generated JVM checks, bound heap growth with -J-Xmx1536m (adjust
|
||||
only if a run reports a real heap limit failure). Several uncapped JVMs can
|
||||
fill host RAM and swap and make browser checks time out before tests start.
|
||||
Keep concurrency low; use fresh processes for test-label isolation.
|
||||
|
||||
Canonical design data keeps unrounded geometry. Matrices are represented
|
||||
by their effect on local shape bounds, so their tolerance has pixel units
|
||||
rather than comparing dimensionless coefficients (F153). Undo snapshots
|
||||
and golden masters keep raw matrices. Use oracle/equivalent? to compare
|
||||
it: the absolute 0.01 tolerance applies only inside shape geometry. Rounding
|
||||
first can split two positions 7e-7 px apart across a 0.005 boundary (F150).
|
||||
Scalar values and touched stay exact. comp/undo-check/same-state? supplies
|
||||
the shared comparison; its undo default remains 1e-10.
|
||||
|
||||
Swapped-head dimensions use the content main; position, rotation, flips,
|
||||
constraints and layout-item inputs use the position source (F151).
|
||||
When sizes differ, expected geometry keeps the position source's
|
||||
untransformed top-left anchor after resizing. Resizing around a rotated
|
||||
corner shifts that anchor and creates a false I4 failure (F167).
|
||||
A detached swapped head that becomes a plain copy loses swap-group without
|
||||
inheriting its former position's groups. Descendants whose refs do not change
|
||||
keep their existing shadow groups (F155).
|
||||
|
||||
For free copy-head placement, expected geometry keeps the placement matrix
|
||||
it applied. Re-decomposing a 0.01px frame can add scale noise that becomes
|
||||
a .011px error in its larger children (F161). Child geometry, scalar values
|
||||
and undo comparisons remain checked; this does not widen their tolerance.
|
||||
|
||||
Detach's shadow expresses inherited geometry in the surviving copy's frame
|
||||
before merging groups (F163). Compute orientation from the source and copy
|
||||
before detaching, never from the resulting child, whose geometry is checked.
|
||||
|
||||
The switch shadow compares numeric geometry of the two mains with float
|
||||
tolerance too (F164). A 1e-12 size difference must keep a resize override;
|
||||
a 1px difference must discard it. Nongeometry values remain exact.
|
||||
|
||||
Expected child placement takes unit axis directions from the parent
|
||||
matrices. Resize already runs through dimensions and constraints; a
|
||||
minimum-size clamp can leave residual matrix scale that must not resize
|
||||
fixed children again (F173). Canonical matrices and undo remain raw. The
|
||||
fixture still rejects a real100→80px fixed-child height change.
|
||||
|
||||
Update-main's shadow uses the source values and the new near reference to
|
||||
predict content overrides when a pushed swap becomes an ordinary nested
|
||||
copy (F174). Read the reference id from the new shape for correspondence,
|
||||
but never read its resulting values or touched groups to decide what the
|
||||
user authored. The negative control corrupts both promoted head and child
|
||||
fills and still predicts the source fills.
|
||||
|
||||
A switch retains orientation supplied by the old component even when the
|
||||
old copy has no geometry override (F175). Predict this choice from the old
|
||||
root and the new expected parent placement. A negative control removes the
|
||||
new root's rotation; the shadow must still expect the old angle. Ordinary
|
||||
position inheritance after swapping back must sync without creating new
|
||||
groups (F176); the same shrunk program runs through the pure and async
|
||||
frontend paths.
|
||||
|
||||
Retained orientation must merge into a geometry group that already carries
|
||||
size (F178). Its presence does not mean the angle was carried: the two
|
||||
variant mains can agree on size and disagree on rotation. The control keeps
|
||||
the expected80px height and90deg angle even if the result loses its angle.
|
||||
|
||||
Duplicate candidates and pure Duplicate nodes use cld/allow-duplicate?, the
|
||||
normal frontend eligibility guard (F181). Finite repeated component nesting
|
||||
can still be unsafe to duplicate inside its main; the portable regression
|
||||
checks refusal, mixed batches and allowed ALT duplication at the page root.
|
||||
This is an operation precondition, not an oracle exception.
|
||||
|
||||
The switch shadow follows an authored swapped near head to its content
|
||||
main, using the model's swap group and component identity (F184). Following
|
||||
the replaced position instead can lose an inherited fill override. Keep
|
||||
these choices independent of the resulting touched set and values. The
|
||||
negative control ignores a different fill on the replaced position and
|
||||
still expects the authored fill if the switch result is corrupted.
|
||||
|
||||
@ -76,8 +76,8 @@ Layout:
|
||||
real frontend), `comp/sync_test.cljs` (one `t/async` deftest per shared case; registered in
|
||||
`frontend_tests/runner.cljs`).
|
||||
- JVM/JS: `common-tests.logic.composable-sync-test` runs the shared cases through the pure
|
||||
runner; all except those that need frontend code: N, Z, AI and the rotation case (placement
|
||||
step), X (pending sync), AJ (reflow), AT, AU, AV, AX (variant action events).
|
||||
runner; all except those that need frontend code: Z and AI (layout reflow), X (pending sync), AJ (reflow), AT, AU, AV, AX
|
||||
(variant action events). N and the rotation case also run on the JVM.
|
||||
|
||||
Judging (both runners): each variant is judged before the next one runs (`r/run-each`, and the
|
||||
interpreter's `check`), while the label map still names its shapes; a variant whose inline
|
||||
@ -109,8 +109,10 @@ Two pure runners (no store, JVM + CLJS):
|
||||
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 (sync +
|
||||
undo); the interpreter sends `dwl/reset-component`. `{:sync? false}` disables sync. `Rotate`/`ChangeHeight`/`MoveShape`
|
||||
are `:frontend-only` and throw `::runner/frontend-only` (their modifiers come from frontend events; the placement step itself is common, `app.common.logic.placement`). Swap and switch nodes use the production
|
||||
undo); the interpreter sends `dwl/reset-component`. `{:sync? false}` disables sync.
|
||||
`Rotate` and `ChangeHeight` are shared user operations. Their pure modifiers
|
||||
use CLJC constraints and `app.common.logic.placement`; the frontend interpreter
|
||||
still drives the workspace events. `MoveShape` remains frontend-only. 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
|
||||
(token propagation, WASM text resize, layout update). Tests:
|
||||
@ -280,8 +282,8 @@ reproduce what the real measurement commits (F37: `commit-position-data`).
|
||||
as an error of its case and the next variant runs. `(optional (in-sequence …))` is not flattened for the interpreter — use
|
||||
independent optionals (nested choices are fine). The Serena/clj-kondo cache for `nodes.cljc` goes stale (phantom symbols) —
|
||||
trust the build. Cross-namespace global-state leaks land in this suite first; suspect them before
|
||||
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed RPC
|
||||
that fails headless (benign; absorbed by per-op grace).
|
||||
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed library-status RPC. Fixed and generated
|
||||
cases mock RPC and the delay timer for the whole async replay.
|
||||
|
||||
---
|
||||
|
||||
@ -313,7 +315,9 @@ Distinguishing abstractions (the OOP articulation of the shared principles):
|
||||
each other. `OpCreateNestableComponent(strategy, name)` prefixes its roles with `name`; give
|
||||
every extra lineage in a case a name.
|
||||
- `OpDetach`/`OpSwap`/`OpReset` wait for pending propagation before acting (a script could
|
||||
otherwise act inside the sync window, which no user can). `swapComponent` keeps touched (F33).
|
||||
otherwise act inside the sync window, which no user can). `swapComponent` follows the UI:
|
||||
ordinary swaps clear content overrides and retain orientation; switches
|
||||
within one variant set carry matching overrides.
|
||||
The suite pins published `@penpot/plugin-types` 1.4.2, which lacks `resetOverrides` (typed cast).
|
||||
- Known bugs: `Assert.knownFailure(bug, expected, pinned, actual, equals)` (same order as `tm/known-failure`), the counterpart of
|
||||
`tm/known-failure`.
|
||||
@ -335,3 +339,37 @@ Details: README, "Running in CI".
|
||||
## Substrate
|
||||
`mem:common/testing`, `mem:common/component-data-model`, `mem:common/component-swap-pipeline`,
|
||||
`mem:frontend/testing`.
|
||||
|
||||
|
||||
## Seeded random replay
|
||||
|
||||
`common-tests.logic.composable-gen-test` checks EDN programs through common.
|
||||
`frontend-tests.composable-tests.generated-test/replay` accepts the same EDN
|
||||
(including a shrunk failure), checks target/skip traces, and drives real events
|
||||
with per-step barriers and undo/redo. Its independent model maps shapes between
|
||||
backends by file/page/tree path, including auxiliary library files. UpdateMain
|
||||
passes the target shape's component-file to update-component-sync. Refresh both
|
||||
the working file and libraries after each step; strip installed library-of
|
||||
metadata only from the portable situation, never from undo snapshots.
|
||||
|
||||
Plugin `RandomCompositions` uses the real Plugin API and its own visible fill
|
||||
and opacity model. It waits for waitForLayoutUpdate with a hard deadline;
|
||||
there is no fixed-delay fallback. Live WASM checks use ws1 and enable plugin
|
||||
flags naturalChildOrdering and throwValidationErrors. When browser automation
|
||||
runs in a background tab, foreground it (and use CDP focus emulation if needed)
|
||||
before diagnosing a timeout: background timers can wait a minute per step.
|
||||
Verify initialized WASM, no workspace exception and empty pending reflow.
|
||||
|
||||
Rotate nodes take a delta angle. The interpreter must pass delta?true to
|
||||
increase-rotation; its default sets an absolute angle. A45deg turn followed
|
||||
by90deg must end135deg on both backends (F171).
|
||||
|
||||
## Suite choice after Phase 4
|
||||
|
||||
Keep CLJS replay as the composable regression suite, backed by shared common
|
||||
generation and test.check shrinking. Keep the plugin for public API, WASM,
|
||||
font loading and measured-text smoke checks. Retain its fixed cases until
|
||||
equivalent coverage replaces them; do not build a third full random model
|
||||
for parity. Pure common checks cannot expose async undo/measurement order
|
||||
errors such as F137/F152. Compare coverage and CI cost separately: timings
|
||||
collected during stress runs do not prove either suite is faster.
|
||||
|
||||
@ -56,7 +56,22 @@
|
||||
file (cond-> file (= layout :grid) (tho/place-in-grid-columns root 2 {1 child}))]
|
||||
(tm/with-file situation file))))
|
||||
|
||||
|
||||
(defn- with-library
|
||||
[situation]
|
||||
(let [library (-> (tm/make-situation (thf/sample-file :random-library :is-shared true))
|
||||
(as-> s (tm/apply-to (n/create-component "library-random" setup/labeled-fill :size 100) s))
|
||||
(as-> s (tm/apply-to (n/make-nested-component "library-random" 1) s))
|
||||
tm/file)
|
||||
components (sort-by (comp str :id) (vals (get-in library [:data :components])))
|
||||
file (reduce (fn [file [index component copy]]
|
||||
(thc/instantiate-component
|
||||
file (thi/label (:id component))
|
||||
(keyword (str "random-library-copy-" index "-" copy))
|
||||
:library library))
|
||||
(tm/file situation)
|
||||
(for [[index component] (map-indexed vector components) copy (range 2)]
|
||||
[index component copy]))]
|
||||
(-> situation (tm/with-file file) (tm/with-aux-files {(:id library) library}))))
|
||||
|
||||
(defn forest
|
||||
"All live page trees, with stable file/page prefixes for model replay."
|
||||
@ -75,34 +90,35 @@
|
||||
(defn world
|
||||
[[components depth opts]]
|
||||
(thi/reset-idmap!)
|
||||
(reduce (fn [s index]
|
||||
(let [name (str "random-" index)]
|
||||
(-> (tm/apply-to (n/create-component name setup/labeled-fill :size 100) s)
|
||||
(with-layout name (when (zero? index) (:layout opts)))
|
||||
(as-> s (reduce #(tm/apply-to %2 %1) s
|
||||
[(n/make-nested-component name depth)
|
||||
(n/instantiate-copy name)
|
||||
(n/instantiate-copy name)])))))
|
||||
(let [s (setup/empty-situation)
|
||||
s (if (:variants opts)
|
||||
(reduce #(tm/apply-to %2 %1) s
|
||||
[(n/create-component "variant" setup/labeled-fill :size 100)
|
||||
(n/make-variant-container "random-variants"
|
||||
[["a" setup/labeled-fill]
|
||||
["b" (if (:variant-diff opts) "#ff0000" setup/labeled-fill)]]
|
||||
:size 100)
|
||||
(n/make-nested-component-with-variant "variant" "random-variants" "a")
|
||||
(n/make-nested-component "variant" 1)
|
||||
(n/instantiate-copy "variant")
|
||||
(n/instantiate-copy "variant")])
|
||||
s)]
|
||||
(cond-> s
|
||||
(:text opts)
|
||||
(tm/with-file (-> (thtx/add-text-component-with-copy (tm/file s) [[["Hello world"]]])
|
||||
(ths/update-shape :main-child :grow-type :auto-height)
|
||||
(ths/update-shape :copy-child :grow-type :auto-height)
|
||||
(ths/update-shape :copy-child :touched nil)))))
|
||||
(range components)))
|
||||
(cond-> (reduce (fn [s index]
|
||||
(let [name (str "random-" index)]
|
||||
(-> (tm/apply-to (n/create-component name setup/labeled-fill :size 100) s)
|
||||
(with-layout name (when (zero? index) (:layout opts)))
|
||||
(as-> s (reduce #(tm/apply-to %2 %1) s
|
||||
[(n/make-nested-component name depth)
|
||||
(n/instantiate-copy name)
|
||||
(n/instantiate-copy name)])))))
|
||||
(let [s (setup/empty-situation)
|
||||
s (if (:variants opts)
|
||||
(reduce #(tm/apply-to %2 %1) s
|
||||
[(n/create-component "variant" setup/labeled-fill :size 100)
|
||||
(n/make-variant-container "random-variants"
|
||||
[["a" setup/labeled-fill]
|
||||
["b" (if (:variant-diff opts) "#ff0000" setup/labeled-fill)]]
|
||||
:size 100)
|
||||
(n/make-nested-component-with-variant "variant" "random-variants" "a")
|
||||
(n/make-nested-component "variant" 1)
|
||||
(n/instantiate-copy "variant")
|
||||
(n/instantiate-copy "variant")])
|
||||
s)]
|
||||
(cond-> s
|
||||
(:text opts)
|
||||
(tm/with-file (-> (thtx/add-text-component-with-copy (tm/file s) [[["Hello world"]]])
|
||||
(ths/update-shape :main-child :grow-type :auto-height)
|
||||
(ths/update-shape :copy-child :grow-type :auto-height)
|
||||
(ths/update-shape :copy-child :touched nil)))))
|
||||
(range components))
|
||||
(:library opts) (with-library)))
|
||||
|
||||
(defn candidates
|
||||
[situation model kind scope]
|
||||
@ -430,3 +446,10 @@
|
||||
:ops (into [[:edit 0 0 1 1] [:switch 0] [:switch 0]
|
||||
[:swap 0] [:undo] [:redo] [:reset 0]
|
||||
[:edit 0 1 1 1] [:update-main 0] [:sync 1]] ops)}))
|
||||
|
||||
(def library-program-gen
|
||||
(gen/let [ops (gen/vector (gen/tuple (gen/elements (into structure-kinds [:swap :switch :library-sync]))
|
||||
gen/nat gen/nat gen/nat gen/nat) 0 20)]
|
||||
{:world [2 1 {:library true :variants true}]
|
||||
:ops (into [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]] ops)}))
|
||||
|
||||
@ -162,6 +162,43 @@
|
||||
(t/is (nil? (:variant-id shape)))
|
||||
(t/is (nil? (get-in file [:data :components (:component-id shape) :variant-id]))))))
|
||||
|
||||
(t/deftest generated-library-world-has-remote-mains-and-local-copies
|
||||
(let [situation (cg/world [2 1 {:library true}])
|
||||
file (tm/file situation)
|
||||
libraries (tm/aux-files situation)
|
||||
remote (filter #(contains? libraries (:component-file (:shape %))) (cg/tree situation))]
|
||||
(t/is (= 1 (count libraries)))
|
||||
(t/is (seq remote))
|
||||
(t/is (every? :shape-ref (map :shape remote)))
|
||||
(t/is (not (contains? libraries (:id file))))))
|
||||
|
||||
(t/deftest generated-library-update-main-checks-remote-and-local-inverses
|
||||
(let [program {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:sync 1]]}
|
||||
result (cg/run program)]
|
||||
(t/is (= 1 (get-in result [:executed :update-main])))
|
||||
(t/is (= 1 (get-in result [:executed :library-sync])))
|
||||
(t/is (= 1 (count (tm/aux-files (:situation result)))))))
|
||||
|
||||
(t/deftest random-library-compositions
|
||||
(let [coverage (atom {})
|
||||
failure (atom nil)]
|
||||
(smt/check!
|
||||
(smt/for [program cg/library-program-gen]
|
||||
(cg/check-program program coverage failure))
|
||||
:max-size 30)
|
||||
(println "Executed library operations:" (pr-str @coverage))
|
||||
(doseq [kind [:edit :update-main :library-sync :undo :redo]]
|
||||
(t/is (pos? (get @coverage kind 0)) (str "No executed " kind)))))
|
||||
|
||||
(t/deftest update-main-moves-an-authored-swap-to-the-replaced-main-shape
|
||||
(let [result (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 2 0 0 0] [:update-main 0 0 0 0]]})]
|
||||
(t/is (some #(contains? % :swap-group) (vals (:model result))))))
|
||||
|
||||
(t/deftest swap-after-updating-a-rotated-main-is-already-fully-synced
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
@ -169,6 +206,12 @@
|
||||
[:update-main 0] [:sync 1] [:rotate 0 0 0 0]
|
||||
[:update-main 0 0 0 0] [:swap 0 0 0 0] [:restore 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest update-main-after-deleting-a-swapped-main-keeps-the-model-in-step
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:delete 2 0 0 0] [:update-main 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest detach-from-a-deleted-near-main-inherits-only-the-skipped-level
|
||||
(t/is (map? (cg/run {:world [3 2 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -178,6 +221,18 @@
|
||||
[:detach 0] [:undo] [:redo] [:delete 1 0 0 0]
|
||||
[:detach 13 0 0 0]]}))))
|
||||
|
||||
(t/deftest switch-keeps-root-geometry-inherited-through-a-near-main
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:rotate 0 0 0 0] [:switch 1 0 0 0]]}))))
|
||||
|
||||
(t/deftest a-main-detach-removes-an-indirect-copys-invalid-swap
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:detach 0 0 0 0] [:delete 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest undo-after-a-main-child-add-and-root-rotation-is-fully-settled
|
||||
(t/is (map? (cg/run {:world [2 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -196,6 +251,18 @@
|
||||
[:detach 0] [:undo] [:redo] [:rotate 2 0 0 0]
|
||||
[:resize 7 0 0 0] [:duplicate 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest detached-swapped-head-follows-its-own-component-without-position-overrides
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:duplicate 1 0 0 0] [:swap 13 0 0 0] [:detach 16 0 0 0]]}))))
|
||||
|
||||
(t/deftest detaching-an-outer-copy-keeps-its-surviving-inner-swap
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 3 0 0 0] [:detach 3 0 0 0]]}))))
|
||||
|
||||
(t/deftest pushed-swap-followed-by-nesting-undoes-exactly
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
@ -221,6 +288,33 @@
|
||||
[:add-child 1 0 0 0] [:add-child 1 0 0 0]
|
||||
[:swap 0 0 0 0] [:swap 0 0 0 0] [:text 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest updating-main-then-swapping-a-child-restores-parent-state
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5 0 0 0] [:update-main 0 0 0 0] [:swap 13 0 0 0]]}))))
|
||||
|
||||
(t/deftest pushed-variant-subtree-restores-its-main-parent-order
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:resize 0 0 0 0] [:duplicate 0 0 0 0]
|
||||
[:switch 2 0 0 0] [:update-main 2 0 0 0]]}))))
|
||||
|
||||
(t/deftest updating-a-duplicated-switched-copy-restores-the-main-order
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:duplicate 0 0 0 0]
|
||||
[:update-main 0 0 0 0] [:redo 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest switching-a-detached-rotated-copy-keeps-its-resize
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:rotate 1 0 0 0] [:detach 1 0 0 0]
|
||||
[:detach 2 0 0 0] [:resize 2 0 0 0] [:switch 2 0 0 0]]}))))
|
||||
|
||||
(t/deftest minimum-size-parent-orientation-does-not-amplify-oracle-drift
|
||||
(t/is (map? (cg/run {:world [3 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -285,6 +379,12 @@
|
||||
(t/is (= [:green] (:fills expected)))
|
||||
(t/is (false? (shadow/same-value? (:copy after) :fill-group expected))))))
|
||||
|
||||
(t/deftest switching-a-copy-of-a-swap-keeps-position-overrides-inherited
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 13 0 0 0] [:rotate 0 0 0 0] [:switch 2 0 0 0]]}))))
|
||||
|
||||
(t/deftest rotated-swapped-head-keeps-its-position-anchor-with-a-different-content-size
|
||||
(t/is (map? (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
@ -328,6 +428,34 @@
|
||||
(t/is (= {:fills [:white] :hide-fill-on-export nil} (get-in model [id :fill-group])))
|
||||
(t/is (false? (shadow/same-value? (get after id) :fill-group (get-in model [id :fill-group])))))))
|
||||
|
||||
(t/deftest generated-library-pushed-swap-authors-content-against-its-new-position
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 2 0 0 0] [:edit 8 0 0 0] [:switch 0 0 0 0]
|
||||
[:update-main 7 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-swap-back-syncs-the-near-positions-overrides
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 15 0 0 0] [:duplicate 16 0 0 0] [:edit 8 0 0 0]
|
||||
[:switch 4 0 0 0] [:update-main 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-move-restores-existing-component-nesting-on-undo
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5 0 0 0] [:nest 8 0 0 0] [:nest 5 0 0 0]
|
||||
[:restore 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-authored-child-follows-its-parents-synced-turn
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:resize 0 0 0 0] [:switch 2 0 0 0] [:rotate 1 0 0 0]
|
||||
[:remove-child 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest switch-shadow-keeps-inherited-orientation-even-if-the-result-loses-it
|
||||
(let [before {:page {:id :page}
|
||||
:main-a {:id :main-a :parent-id :page :name "A" :rotation 45}
|
||||
@ -369,6 +497,12 @@
|
||||
[:nest 3 0 0 0] [:swap 10 0 0 0] [:resize 3 0 0 0]
|
||||
[:update-main 5 0 0 0] [:switch 0 0 0 0] [:detach 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest pushing-a-swap-keeps-the-orientation-inherited-from-its-position
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:rotate 0 0 0 0] [:swap 13 0 0 0] [:update-main 1 0 0 0]]}))))
|
||||
|
||||
(t/deftest resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
|
||||
(t/is (map? (cg/run {:world [3 0 {:text true :layout :flex}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -382,6 +516,52 @@
|
||||
(t/is (map? (cg/run {:world [2 1]
|
||||
:ops [[:rotate 1 0 0 0] [:rotate 1 0 1 0] [:update-main 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest pushing-a-switched-child-uses-its-parents-pushed-frame
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5 0 0 0] [:rotate 1 0 0 0] [:update-main 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-switch-keeps-overrides-from-a-deleted-near-main
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:delete 0 0 0 0] [:delete 3 0 0 0] [:resize 0 0 0 0]
|
||||
[:delete 9 0 0 0] [:switch 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-duplicate-refuses-an-expanding-component-branch
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5] [:nest 8] [:duplicate 1]]}))))
|
||||
|
||||
(t/deftest generated-library-parent-turn-settles-swapped-placement
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 2 0 0 0] [:resize 0 0 0 0] [:rotate 2 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-restored-variant-syncs-its-converted-name
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:delete 3 0 0 0] [:delete 1 0 0 0] [:restore 7 0 0 0]
|
||||
[:delete 1 0 0 0] [:restore 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-switch-inherits-a-swapped-near-mains-authored-fill
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:edit 12 0 0 0] [:nest 10 0 0 0]
|
||||
[:switch 7 0 0 0] [:switch 2 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-library-reset-places-new-children-after-clearing-rotation
|
||||
(t/is (map? (cg/run {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:rotate 1 0 0 0] [:duplicate 0 0 0 0]
|
||||
[:reset 2 0 0 0] [:redo 0 0 0 0]]}))))
|
||||
|
||||
(t/deftest generated-update-main-settles-a-turned-swapped-position
|
||||
(let [result (cg/run {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.comp.nodes :as n]
|
||||
[app.common.test-helpers.composable.comp.runner :as r]
|
||||
[app.common.test-helpers.composable.comp.setups :as setup]
|
||||
@ -619,6 +620,22 @@
|
||||
(t/is (= ::tm/unbound-shape-name (:type (ex-data error))))
|
||||
(t/is (some #{:copy-child} (:present (ex-data error))))))
|
||||
|
||||
(t/deftest library-update-main-routes-both-files-and-undoes-the-cascade
|
||||
(let [initial (setup/cross-file-component-with-copy setup/labeled-fill)
|
||||
library (first (vals (tm/aux-files initial)))
|
||||
initial (tm/with-file initial
|
||||
(thc/instantiate-component (tm/file initial) :component1 :copy2-root
|
||||
:library library :children-labels [:copy2-child]))
|
||||
edited (-> initial (r/step (n/change-property :copy-child :fills "#ff0000")) r/settle)
|
||||
updated (-> edited (r/step (n/update-main :copy-root)) r/settle)
|
||||
remote (get (tm/aux-files updated) (:id library))
|
||||
snapshot (fn [s] (uc/snapshot-files (assoc (tm/aux-files s) (:id (tm/file s)) (tm/file s))))]
|
||||
(t/is (= "#ff0000" (get-in (thf/current-page remote) [:objects (thi/id :main-child) :fills 0 :fill-color])))
|
||||
(t/is (= "#ff0000" (:fill-color (first (:fills (tm/resolve-shape updated :copy2-child))))))
|
||||
(t/is (nil? (:touched (tm/resolve-shape updated :copy-child))))
|
||||
(t/is (= (snapshot edited) (snapshot (r/step updated (n/undo)))))
|
||||
(t/is (= (snapshot updated) (snapshot (-> updated (r/step (n/undo)) (r/step (n/redo))))))))
|
||||
|
||||
(t/deftest adding-a-grid-child-records-cell-order-before-reflow
|
||||
(let [initial (setup/grid-component-with-copy)
|
||||
changed (-> initial (r/step (n/add-child :grid-root :new-grid-child)) r/settle)
|
||||
|
||||
@ -103,6 +103,31 @@
|
||||
(doseq [seed [712 20261022]]
|
||||
(await (replay (gen/generate cg/swap-program-gen 5 seed)))))
|
||||
|
||||
(t/deftest ^:async library-programs-replay-through-the-frontend
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:sync 1]]}))
|
||||
(await (replay (gen/generate cg/library-program-gen 5 712))))
|
||||
|
||||
(t/deftest ^:async inherited-rotation-switch-shrunk-regression
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:rotate 0 0 0 0] [:switch 1 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async swapped-subtree-update-main-shrunk-regression
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:resize 0 0 0 0] [:duplicate 0 0 0 0]
|
||||
[:switch 2 0 0 0] [:update-main 2 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async main-head-detach-shrunk-regression
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:detach 0 0 0 0] [:delete 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async partial-sync-geometry-boundary-shrunk-regression
|
||||
(await (replay {:world [2 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -117,6 +142,12 @@
|
||||
:ops [[:duplicate 0] [:nest 0] [:rotate 0 0 0]
|
||||
[:edit 0 0 1 1] [:update-main 0]]})))
|
||||
|
||||
(t/deftest ^:async updating-main-then-swapping-a-child-restores-parent-state
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5 0 0 0] [:update-main 0 0 0 0] [:swap 13 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async swapping-main-subtrees-with-several-children-undoes-their-order
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
@ -135,6 +166,20 @@
|
||||
[:resize 0 0 0 0] [:nest 0 0 0 0] [:rotate 4 0 0 0]
|
||||
[:detach 3 0 0 0] [:update-main 4 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async updating-a-duplicated-switched-copy-restores-the-main-order
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:duplicate 0 0 0 0]
|
||||
[:update-main 0 0 0 0] [:redo 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async switching-a-detached-rotated-copy-keeps-its-resize
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:rotate 1 0 0 0] [:detach 1 0 0 0]
|
||||
[:detach 2 0 0 0] [:resize 2 0 0 0] [:switch 2 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async minimum-size-parent-orientation-does-not-amplify-oracle-drift
|
||||
(await (replay {:world [3 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -154,12 +199,24 @@
|
||||
[:detach 0] [:undo] [:redo] [:resize 0 0 0 0]
|
||||
[:delete 0 0 0 0] [:rotate 0 0 0 0] [:detach 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async switching-a-copy-of-a-swap-keeps-position-overrides-inherited
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 13 0 0 0] [:rotate 0 0 0 0] [:switch 2 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async rotated-swapped-head-keeps-its-position-anchor-with-a-different-content-size
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
|
||||
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1] [:update-main 0]
|
||||
[:sync 1] [:swap 17 0 0 0] [:resize 0 0 0 0] [:rotate 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async pushing-a-swap-keeps-the-orientation-inherited-from-its-position
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:rotate 0 0 0 0] [:swap 13 0 0 0] [:update-main 1 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
|
||||
(await (replay {:world [3 0 {:text true :layout :flex}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -173,6 +230,12 @@
|
||||
(await (replay {:world [2 1]
|
||||
:ops [[:rotate 1 0 0 0] [:rotate 1 0 1 0] [:update-main 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async pushing-a-switched-child-uses-its-parents-pushed-frame
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5 0 0 0] [:rotate 1 0 0 0] [:update-main 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async minimum-size-parent-placement-keeps-fixed-child-dimensions
|
||||
(await (replay {:world [2 0 {:text true :layout nil}]
|
||||
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
|
||||
@ -182,6 +245,41 @@
|
||||
[:detach 0] [:undo] [:redo] [:resize 1 0 0 0]
|
||||
[:nest 2 0 0 0] [:rotate 0 0 3 0] [:edit 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-pushed-swap-authors-content-against-its-new-position
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 2 0 0 0] [:edit 8 0 0 0] [:switch 0 0 0 0]
|
||||
[:update-main 7 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-swap-back-syncs-the-near-positions-overrides
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 15 0 0 0] [:duplicate 16 0 0 0] [:edit 8 0 0 0]
|
||||
[:switch 4 0 0 0] [:update-main 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-move-restores-existing-component-nesting-on-undo
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5 0 0 0] [:nest 8 0 0 0] [:nest 5 0 0 0]
|
||||
[:restore 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-authored-child-follows-its-parents-synced-turn
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:resize 0 0 0 0] [:switch 2 0 0 0] [:rotate 1 0 0 0]
|
||||
[:remove-child 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-switch-keeps-overrides-from-a-deleted-near-main
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:delete 0 0 0 0] [:delete 3 0 0 0] [:resize 0 0 0 0]
|
||||
[:delete 9 0 0 0] [:switch 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async switching-an-untouched-rotated-copy-survives-full-sync
|
||||
(await (replay {:world [3 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
@ -198,6 +296,39 @@
|
||||
[:nest 0 0 0 0] [:detach 0 0 0 0] [:update-main 0 0 0 0]
|
||||
[:switch 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-duplicate-refuses-an-expanding-component-branch
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 5] [:nest 8] [:duplicate 1]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-parent-turn-settles-swapped-placement
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 2 0 0 0] [:resize 0 0 0 0] [:rotate 2 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-restored-variant-syncs-its-converted-name
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:delete 3 0 0 0] [:delete 1 0 0 0] [:restore 7 0 0 0]
|
||||
[:delete 1 0 0 0] [:restore 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-switch-inherits-a-swapped-near-mains-authored-fill
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:edit 12 0 0 0] [:nest 10 0 0 0]
|
||||
[:switch 7 0 0 0] [:switch 2 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-library-reset-places-new-children-after-clearing-rotation
|
||||
(await (replay {:world [2 1 {:library true :variants true}]
|
||||
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
|
||||
[:undo] [:redo] [:reset 0] [:sync 1]
|
||||
[:switch 1 0 0 0] [:rotate 1 0 0 0] [:duplicate 0 0 0 0]
|
||||
[:reset 2 0 0 0] [:redo 0 0 0 0]]})))
|
||||
|
||||
(t/deftest ^:async generated-update-main-settles-a-turned-swapped-position
|
||||
(await (replay {:world [2 2 {:variants true :variant-diff false}]
|
||||
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
|
||||
|
||||
@ -9,8 +9,9 @@ import { PenpotSync } from "../util/PenpotSync";
|
||||
* component, via the Plugin API's `swapComponent()`. The new head keeps the
|
||||
* old head's id, so a target that found the old head finds the new one.
|
||||
*
|
||||
* Unlike the swap in the workspace UI, the API keeps the head's overrides
|
||||
* (it swaps with keep-touched, F33 in the stabilization plan).
|
||||
* The API follows the workspace rules: ordinary swaps clear content
|
||||
* overrides and retain orientation. A switch between variants of one set
|
||||
* carries matching overrides.
|
||||
*/
|
||||
export class OpSwap extends Operation {
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user