mirror of
https://github.com/penpot/penpot.git
synced 2026-10-30 14:30:37 +00:00
🐛 Track authored grid cells in the component oracle
Compare overridden cells with independent before-state and intent values, while keeping copy child structure tied to the main. Remap child identities across duplication, update-main, detach and frontend replay. Direction edits track both changed groups. The former I4 reproduction stays unchanged. Add grid actions and seven histories with exact undo/redo, plus controls for corrupt values, membership, groups, child order and dropped main edits. Keep random grid rollout pending track growth and switch carry coverage. Validation: common JVM 2,009/32,967; JS 1,945/31,582; frontend 1,749/13,838; backend 1,793/9,007; exporter 16/40. All suites, lint and format pass. Six properties run 40 programs at seed 712 in both runtimes. AI-assisted-by: gpt-6
This commit is contained in:
parent
b84411c5c9
commit
7d2c20b804
@ -221,6 +221,39 @@
|
||||
[target property value]
|
||||
(tm/assign-id (->ChangeProperty target property value)))
|
||||
|
||||
(defrecord EditGrid [target action value]
|
||||
IComponentOperation
|
||||
(op-kind [_] :user)
|
||||
|
||||
tm/IOperation
|
||||
(apply-to [this situation]
|
||||
(let [file (tm/file situation)
|
||||
page (thf/current-page file)
|
||||
id (tm/target-shape-id situation target)
|
||||
edit (fn [shape]
|
||||
(case action
|
||||
:direction (let [from (or (:layout-grid-dir shape) :row)]
|
||||
(if (= from value)
|
||||
(assoc shape :layout-grid-dir value)
|
||||
(ctl/reflow-grid-auto-items-for-direction shape from value)))
|
||||
:cell (update shape :layout-grid-cells
|
||||
(fn [cells]
|
||||
(update-vals cells #(if (= [(:row value) (:column value)] [(:row %) (:column %)])
|
||||
(merge % (:props value)) %))))))
|
||||
changes (-> (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) [id]
|
||||
edit (:objects page)
|
||||
{:skip-grid-reassignment? (= action :direction)})
|
||||
(with-layout-reflow id)
|
||||
(assoc ::layout-reflow? true))]
|
||||
(-> situation
|
||||
(tm/with-file (thf/apply-changes file changes))
|
||||
(record-changes changes)
|
||||
(tm/record-application this {:target target :action action :value value})))))
|
||||
|
||||
(defn edit-grid
|
||||
[target action value]
|
||||
(tm/assign-id (->EditGrid target action value)))
|
||||
|
||||
(defprotocol IPropertyCheck
|
||||
"The check dual of an operation that changes a property: whether a shape the
|
||||
test supplies holds the value the operation set."
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
[app.common.test-helpers.composable.comp.runner :as r]
|
||||
[app.common.test-helpers.composable.comp.setups :as setup]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.composable.grid :as grid]
|
||||
[app.common.test-helpers.composable.oracle :as oracle]
|
||||
[app.common.test-helpers.composable.shadow :as shadow]
|
||||
[app.common.test-helpers.composable.variants :as variants]
|
||||
@ -98,6 +99,23 @@
|
||||
situation ids)
|
||||
(with-settled-layout name))))
|
||||
|
||||
(defn- with-square-grid
|
||||
[situation name]
|
||||
(let [id ((n/main-head-of name) situation)
|
||||
shape (tm/shape-by-id situation id)
|
||||
children (vec (reverse (:shapes shape)))
|
||||
cells (into {} (map-indexed (fn [index [row column]]
|
||||
(let [id (uuid/next)]
|
||||
[id {:id id :row row :column column :row-span 1 :column-span 1
|
||||
:position :auto :align-self :auto :justify-self :auto
|
||||
:shapes (if-let [child (get children index)] [child] [])}]))
|
||||
[[1 1] [1 2] [2 1] [2 2]]))
|
||||
file (-> (tm/file situation)
|
||||
(ths/update-shape-by-id (:id shape) :layout-grid-rows [{:type :fixed :value 60} {:type :fixed :value 60}])
|
||||
(ths/update-shape-by-id (:id shape) :layout-grid-columns [{:type :fixed :value 80} {:type :fixed :value 80}])
|
||||
(ths/update-shape-by-id (:id shape) :layout-grid-cells cells))]
|
||||
(with-settled-layout (tm/with-file situation file) name)))
|
||||
|
||||
(defn- with-outer-layout
|
||||
[situation name layout]
|
||||
(if-not layout
|
||||
@ -214,6 +232,8 @@
|
||||
(with-layout-children name))
|
||||
(cond-> (and (zero? index) (:layout opts) (:layout-item-margins opts))
|
||||
(with-layout-item-margins name))
|
||||
(cond-> (and (zero? index) (= :grid (:layout opts)) (:grid-square opts))
|
||||
(with-square-grid name))
|
||||
(as-> s (reduce #(tm/apply-to %2 %1) s
|
||||
[(n/make-nested-component name depth)]))
|
||||
(with-outer-layout name (when (and (zero? index) (pos? depth) (:layout-copies opts)) (:layout opts)))
|
||||
@ -473,6 +493,10 @@
|
||||
[:direction ["ltr" "rtl"]]
|
||||
[:valign ["top" "bottom"]]])
|
||||
|
||||
(def grid-cell-edits
|
||||
[{:align-self :end} {:justify-self :center} {:align-self :center :justify-self :end}
|
||||
{:align-self :auto :justify-self :auto}])
|
||||
|
||||
(defn- edit-text-style
|
||||
[shape unit value]
|
||||
(let [[style values] (nth text-style-edits (mod (or unit 0) (count text-style-edits)))
|
||||
@ -495,7 +519,10 @@
|
||||
[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 scope)
|
||||
(candidates situation model (if (= kind :style) :edit kind) scope))
|
||||
(candidates situation model (if (#{:style :grid-cell :grid-direction} kind) :edit kind) scope))
|
||||
(#{:grid-cell :grid-direction} kind)
|
||||
(filter #(and (= :grid (get-in % [:shape :layout]))
|
||||
(seq (get-in % [:shape :layout-grid-cells]))))
|
||||
(= kind :style)
|
||||
(filter (fn [{:keys [shape]}]
|
||||
(and (shadow/attribute-applicable? shape style-property)
|
||||
@ -543,6 +570,11 @@
|
||||
(when target
|
||||
(case kind
|
||||
(:edit :style) (n/change-property target property value)
|
||||
:grid-direction (n/edit-grid target :direction (nth [:row :column] (mod (or (nth tuple 3 nil) 0) 2)))
|
||||
:grid-cell (let [cells (grid/get-cells (:shape selected))
|
||||
cell (nth cells (mod (or (nth tuple 2 nil) 0) (count cells)))
|
||||
props (nth grid-cell-edits (mod (or (nth tuple 3 nil) 0) (count grid-cell-edits)))]
|
||||
(n/edit-grid target :cell (assoc (select-keys cell [:row :column]) :props props)))
|
||||
(: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)]
|
||||
@ -747,6 +779,7 @@
|
||||
id (:id shape)]
|
||||
(case kind
|
||||
(:edit :style) (shadow/edit model shape property value)
|
||||
(:grid-cell :grid-direction) (grid/edit model shape (:action op) (:value op))
|
||||
:reset (shadow/clear-subtree model objects id)
|
||||
:update-main (shadow/update-main model objects (all-objects after) id)
|
||||
:detach (shadow/detach model objects (all-objects after) id)
|
||||
@ -842,6 +875,9 @@
|
||||
(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 (#{:grid-cell :grid-direction} kind)
|
||||
(oracle/check-requested-grid (tm/shape-by-id after (get-in resolved [:shape :id]))
|
||||
(:shape resolved) (:action op) (:value op)))
|
||||
(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))
|
||||
|
||||
105
common/src/app/common/test_helpers/composable/grid.cljc
Normal file
105
common/src/app/common/test_helpers/composable/grid.cljc
Normal file
@ -0,0 +1,105 @@
|
||||
;; 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 app.common.test-helpers.composable.grid
|
||||
"Independent cell expectations. No production grid assignment or direction
|
||||
helper supplies expected values.")
|
||||
|
||||
(defn get-cells
|
||||
[shape]
|
||||
(->> (vals (:layout-grid-cells shape))
|
||||
(sort-by (juxt :row :column))
|
||||
(mapv #(dissoc % :id))))
|
||||
|
||||
(defn remap-groups
|
||||
[groups ids-map]
|
||||
(cond-> groups
|
||||
(:layout-grid-cells groups)
|
||||
(update-in [:layout-grid-cells :layout-grid-cells]
|
||||
(fn [cells]
|
||||
(mapv #(update % :shapes (fn [ids] (mapv (fn [id] (get ids-map id id)) ids))) cells)))))
|
||||
|
||||
(defn get-child-map
|
||||
"Pair identities through refs, never through resulting cell membership."
|
||||
[objects source target]
|
||||
(let [source-ids (set (:shapes source))
|
||||
target-ids (set (:shapes target))
|
||||
find-ref (fn [ids id]
|
||||
(loop [candidate id seen #{}]
|
||||
(cond
|
||||
(contains? ids candidate) candidate
|
||||
(or (nil? candidate) (contains? seen candidate)) nil
|
||||
:else (recur (:shape-ref (get objects candidate)) (conj seen candidate)))))
|
||||
forward (into {} (keep #(when-let [id (find-ref target-ids %)] [% id])) (:shapes source))
|
||||
backward (into {} (keep #(when-let [id (find-ref source-ids %)] [id %])) (:shapes target))]
|
||||
(merge forward backward)))
|
||||
|
||||
(defn direction-cells
|
||||
[shape direction]
|
||||
(let [cells (get-cells shape)
|
||||
order (fn [dir] (if (= dir :column) (juxt :column :row) (juxt :row :column)))
|
||||
eligible (fn [cell] (and (= :auto (:position cell)) (= 1 (:row-span cell) (:column-span cell))))
|
||||
old (filter eligible (sort-by (order (:layout-grid-dir shape)) cells))
|
||||
new (filter eligible (sort-by (order direction) cells))
|
||||
members (vec (mapcat :shapes old))
|
||||
by-cell (into {} (map-indexed (fn [index cell]
|
||||
[[(:row cell) (:column cell)]
|
||||
(if-let [id (nth members index nil)] [id] [])]) new))]
|
||||
(if (= (or (:layout-grid-dir shape) :row) direction)
|
||||
cells
|
||||
(mapv (fn [cell]
|
||||
(if-let [ids (get by-cell [(:row cell) (:column cell)])]
|
||||
(assoc cell :shapes ids)
|
||||
cell)) cells))))
|
||||
|
||||
(defn get-intended-values
|
||||
[shape action value]
|
||||
(let [before (get-cells shape)
|
||||
cells (case action
|
||||
:direction (direction-cells shape value)
|
||||
:cell (mapv #(if (= [(:row value) (:column value)] [(:row %) (:column %)])
|
||||
(merge % (:props value)) %) before))]
|
||||
(cond-> {:layout-grid-cells cells}
|
||||
(= action :direction) (assoc :layout-grid-dir value))))
|
||||
|
||||
(defn edit
|
||||
[model shape action value]
|
||||
(if (:shape-ref shape)
|
||||
(reduce-kv (fn [model key value]
|
||||
(if (= value (if (= key :layout-grid-cells) (get-cells shape) (get shape key)))
|
||||
model
|
||||
(assoc-in model [(:id shape) key] {key value})))
|
||||
model (get-intended-values shape action value))
|
||||
model))
|
||||
|
||||
(defn settle-cells
|
||||
"Drop departed/absolute children; fill existing empty cells with participating
|
||||
children in parent order. A gap, manual cell or span ends the auto prefix."
|
||||
[cells shape objects]
|
||||
(let [order (if (= :column (:layout-grid-dir shape)) (juxt :column :row) (juxt :row :column))
|
||||
participating (filterv #(let [child (get objects %)]
|
||||
(not (or (:hidden child) (:layout-item-absolute child)))) (:shapes shape))
|
||||
members (set participating)
|
||||
prefix (fn [cells]
|
||||
(second
|
||||
(reduce (fn [[ended result] cell]
|
||||
(let [manual (or (= :manual (:position cell))
|
||||
(not= 1 (:row-span cell) (:column-span cell))
|
||||
(and ended (seq (:shapes cell)) (= :auto (:position cell))))]
|
||||
[(or ended manual (empty? (:shapes cell)))
|
||||
(conj result (cond-> cell
|
||||
(and manual (= :auto (:position cell))) (assoc :position :manual)))]))
|
||||
[false []] cells)))
|
||||
cells (->> cells (sort-by order)
|
||||
(mapv #(update % :shapes (fn [ids] (filterv members ids)))) prefix)
|
||||
assigned (set (mapcat :shapes cells))
|
||||
missing (remove assigned participating)
|
||||
[cells _] (reduce (fn [[result pending] cell]
|
||||
(if (and (seq pending) (empty? (:shapes cell)))
|
||||
[(conj result (assoc cell :position :auto :shapes [(first pending)])) (rest pending)]
|
||||
[(conj result cell) pending]))
|
||||
[[] missing] cells)]
|
||||
(->> cells prefix (sort-by (juxt :row :column)) vec)))
|
||||
@ -16,6 +16,7 @@
|
||||
[app.common.test-helpers.composable.comp.undo-check :as uc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.composable.geometry :as geom]
|
||||
[app.common.test-helpers.composable.grid :as grid]
|
||||
[app.common.test-helpers.composable.shadow :as shadow]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.normalize :as thn]
|
||||
@ -33,6 +34,14 @@
|
||||
(throw (ex-info (str "Component invariant " (name invariant) " failed")
|
||||
(assoc details :invariant invariant))))
|
||||
|
||||
(defn check-requested-grid
|
||||
[shape before action value]
|
||||
(let [expected (grid/get-intended-values before action value)
|
||||
actual (cond-> {:layout-grid-cells (grid/get-cells shape)}
|
||||
(= action :direction) (assoc :layout-grid-dir (:layout-grid-dir shape)))]
|
||||
(when-not (= expected actual)
|
||||
(fail :I9 {:phase :requested-grid :expected expected :actual actual}))))
|
||||
|
||||
(defn snapshot
|
||||
[situation]
|
||||
(into {} (map (fn [[id file]]
|
||||
@ -355,12 +364,14 @@
|
||||
(->> cells end-auto-prefix (sort-by (juxt :row :column)) vec)))
|
||||
|
||||
(defn- check-structure
|
||||
[objects shape main]
|
||||
[objects shape main authored]
|
||||
(let [refs (mapv #(get-in objects [% :shape-ref]) (:shapes shape))]
|
||||
(when-not (= (vec (:shapes main)) refs)
|
||||
(fail :I4 {:shape (:name shape) :group :structure
|
||||
:expected (:shapes main) :actual refs}))
|
||||
(when (= :grid (:layout main))
|
||||
;; Authored cells have already been checked against independent intent
|
||||
;; by I9. Child structure still follows the main in every case.
|
||||
(when (and (= :grid (:layout main)) (not (contains? authored :layout-grid-cells)))
|
||||
(let [expected (expected-grid-cells objects shape main)
|
||||
actual (grid-cells shape)]
|
||||
(when-not (= expected actual)
|
||||
@ -414,7 +425,7 @@
|
||||
:expected value :actual (shadow/values shape group)})))
|
||||
(when (and settled (:shape-ref shape))
|
||||
(when-let [main (ctf/find-content-ref-shape file container libraries shape :include-deleted? true)]
|
||||
(check-structure (:objects page) shape main)
|
||||
(check-structure (:objects page) shape main expected)
|
||||
(doseq [group (keys shadow/attributes)
|
||||
:when (and (not= group :geometry-group) (not (contains? touched group)))
|
||||
:when (not (and (= group :name-group) (= :text (:type shape))
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
[app.common.math :as mth]
|
||||
[app.common.test-helpers.composable.comp.nodes :as n]
|
||||
[app.common.test-helpers.composable.geometry :as geom]
|
||||
[app.common.test-helpers.composable.grid :as grid]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.types.container :as ctn]))
|
||||
|
||||
@ -104,9 +105,11 @@
|
||||
|
||||
(defn values
|
||||
[shape group]
|
||||
(if (contains? text-groups group)
|
||||
(cond
|
||||
(= group :layout-grid-cells) {:layout-grid-cells (grid/get-cells shape)}
|
||||
(contains? text-groups group)
|
||||
{:content (:content shape)}
|
||||
(into {} (map #(vector % (get shape %))) (get attributes group))))
|
||||
:else (into {} (map #(vector % (get shape %))) (get attributes group))))
|
||||
|
||||
(defn author-geometry
|
||||
[shape parent]
|
||||
@ -161,7 +164,8 @@
|
||||
(defn duplicate
|
||||
[model before source-id after copy-id]
|
||||
(letfn [(copy-groups [result source-id copy-id]
|
||||
(let [groups (get model source-id)
|
||||
(let [groups (grid/remap-groups (get model source-id)
|
||||
(zipmap (:shapes (get before source-id)) (:shapes (get after copy-id))))
|
||||
result (cond-> result (seq groups) (assoc copy-id groups))]
|
||||
(reduce (fn [result [source child]] (copy-groups result source child)) result
|
||||
(map vector (:shapes (get before source-id)) (:shapes (get after copy-id))))))]
|
||||
@ -202,7 +206,7 @@
|
||||
(loop [shape shape groups (get model (:id shape)) seen #{}]
|
||||
(let [ref (:shape-ref shape)
|
||||
source (get before ref)
|
||||
inherited (get model ref)
|
||||
inherited (grid/remap-groups (get model ref) (grid/get-child-map before source original))
|
||||
inherited (cond-> inherited
|
||||
(:geometry-group inherited)
|
||||
(update :geometry-group in-orientation source original))]
|
||||
@ -305,7 +309,7 @@
|
||||
(assoc id {:swap-group {}})
|
||||
(and (:shape-ref ref) (seq groups))
|
||||
(update (:id ref) merge
|
||||
(cond-> groups
|
||||
(cond-> (grid/remap-groups groups (grid/get-child-map after shape ref))
|
||||
(= (:component-id ref)
|
||||
(:component-id (get after (:shape-ref ref))))
|
||||
(dissoc :swap-group))))]
|
||||
@ -342,7 +346,10 @@
|
||||
ordinary? (and shape position
|
||||
(nil? (:component-id shape))
|
||||
(nil? (:component-id position)))
|
||||
groups (cond-> groups ordinary? (dissoc :swap-group))]
|
||||
groups (cond-> groups
|
||||
ordinary? (dissoc :swap-group)
|
||||
(:layout-grid-cells groups)
|
||||
(update-in [:layout-grid-cells :layout-grid-cells] grid/settle-cells shape objects))]
|
||||
(if (seq groups) (assoc result id groups) (dissoc result id))))
|
||||
model model))
|
||||
|
||||
|
||||
@ -29,6 +29,83 @@
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(t/deftest authored-grid-cell-values-are-checked-against-intent-instead-of-the-main
|
||||
(let [situation (cg/world [1 0 {:layout :grid :layout-children true}])
|
||||
id ((n/nth-copy-head-of "random-0" 0) situation)
|
||||
shape (tm/shape-by-id situation id)
|
||||
cell-id (:id (first (sort-by (juxt :row :column) (vals (:layout-grid-cells shape)))))
|
||||
intended (assoc-in shape [:layout-grid-cells cell-id :align-self] :end)
|
||||
file (tm/file situation)
|
||||
page (thf/current-page file)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) [id]
|
||||
(constantly intended) (:objects page) {})
|
||||
edited (tm/with-file situation (thf/apply-changes file changes))
|
||||
model {id {:layout-grid-cells (shadow/values intended :layout-grid-cells)}}
|
||||
failure (try (oracle/check edited model) nil
|
||||
(catch #?(:clj Exception :cljs js/Error) error (ex-data error)))]
|
||||
(t/is (= #{:layout-grid-cells} (:touched (tm/shape-by-id edited id))))
|
||||
(t/is (nil? failure) (pr-str failure))))
|
||||
|
||||
(t/deftest grid-overrides-survive-component-histories
|
||||
(doseq [ops [[[:grid-direction 0 0 1 1] [:grid-direction 0 0 1 0]
|
||||
[:grid-direction 0 0 0 0] [:update-main 0]
|
||||
[:grid-direction 0 0 0 1] [:reset 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:grid-cell 0 1 1 0] [:grid-cell 0 0 2 0]
|
||||
[:update-main 0] [:grid-cell 0 1 2 1] [:reset 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:duplicate 0] [:undo] [:redo] [:update-main 0]]
|
||||
[[:grid-direction 0 0 1 1] [:add-child 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:remove-child 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]]]
|
||||
(let [result (cg/run {:world [1 0 {:layout :grid :layout-children true :grid-square true}] :ops ops})]
|
||||
(doseq [kind (set (map first ops))]
|
||||
(t/is (zero? (get-in result [:coverage :random kind :skipped] 0)) (str kind " in " ops))
|
||||
(when-not (#{:undo :redo} kind)
|
||||
(t/is (pos? (get-in result [:coverage :random kind :changed] 0)) (str kind " in " ops)))))))
|
||||
|
||||
(t/deftest grid-directions-track-both-groups-and-reject-dropped-main-edits
|
||||
(let [initial (cg/initial-state [1 0 {:layout :grid :layout-children true :grid-square true}])
|
||||
resolved (cg/resolve-op (:situation initial) {} [:grid-direction 0 0 1 1])
|
||||
id (get-in resolved [:shape :id])
|
||||
edited (-> initial (cg/step [:grid-direction 0 0 1 1]) cg/settle-state)
|
||||
file (tm/file (:situation edited))
|
||||
path [:data :pages-index (:id (thf/current-page file)) :objects id]
|
||||
check (fn [file]
|
||||
(try (oracle/check (tm/with-file (:situation edited) file) (:model edited)) nil
|
||||
(catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error)))))
|
||||
main (tm/shape-by-id (:situation initial) (:shape-ref (:shape resolved)))
|
||||
dropped (fn [action value]
|
||||
(try (oracle/check-requested-grid main main action value) nil
|
||||
(catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error)))))]
|
||||
(t/is (= #{:layout-grid-dir :layout-grid-cells} (set (keys (get (:model edited) id)))))
|
||||
(t/is (= :I9 (check (assoc-in file (conj path :layout-grid-dir) :row))))
|
||||
(t/is (= :I3 (check (update-in file (conj path :touched) disj :layout-grid-dir))))
|
||||
(t/is (= :I3 (check (update-in file (conj path :touched) disj :layout-grid-cells))))
|
||||
(t/is (= :I9 (dropped :direction :column)))))
|
||||
|
||||
(t/deftest detaching-an-outer-copy-remaps-inherited-grid-cell-intent
|
||||
(let [result (cg/run {:world [1 1 {:layout :grid :layout-children true :grid-square true}]
|
||||
:ops [[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]})]
|
||||
(t/is (= 1 (get-in result [:coverage :random :grid-cell :changed])))
|
||||
(t/is (= 1 (get-in result [:coverage :random :detach :changed])))))
|
||||
|
||||
(t/deftest grid-intent-rejects-corrupt-values-members-and-missing-groups
|
||||
(let [initial (cg/initial-state [1 0 {:layout :grid :layout-children true :grid-square true}])
|
||||
resolved (cg/resolve-op (:situation initial) {} [:grid-cell 0 0 0 1])
|
||||
id (get-in resolved [:shape :id])
|
||||
cell-id (:id (first (sort-by (juxt :row :column) (vals (get-in resolved [:shape :layout-grid-cells])))))
|
||||
edited (-> initial (cg/step [:grid-cell 0 0 0 1]) cg/settle-state)
|
||||
situation (:situation edited)
|
||||
file (tm/file situation)
|
||||
path [:data :pages-index (:id (thf/current-page file)) :objects id]
|
||||
invariant (fn [file]
|
||||
(try (oracle/check (tm/with-file situation file) (:model edited)) nil
|
||||
(catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error)))))]
|
||||
(t/is (= :I9 (invariant (assoc-in file (into path [:layout-grid-cells cell-id :align-self]) :start))))
|
||||
(t/is (= :I9 (invariant (assoc-in file (into path [:layout-grid-cells cell-id :justify-self]) :end))))
|
||||
(t/is (= :I9 (invariant (assoc-in file (into path [:layout-grid-cells cell-id :shapes]) []))))
|
||||
(t/is (= :I4 (invariant (update-in file (conj path :shapes) #(vec (reverse %))))))
|
||||
(t/is (= :I3 (invariant (update-in file (conj path :touched) disj :layout-grid-cells))))))
|
||||
|
||||
(t/deftest nesting-a-copy-alongside-a-hidden-layout-child-stays-settled
|
||||
(t/is (map? (cg/run {:world [3 0 {:text true :text-styles true :layout :flex}]
|
||||
:ops [[:delete 9 9 1 2] [:text 5 9 1 3] [:style 3 1 1]
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
[app.common.test-helpers.composable.comp.undo-check :as uc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.composable.gen :as cg]
|
||||
[app.common.test-helpers.composable.grid :as grid]
|
||||
[app.common.test-helpers.composable.oracle :as oracle]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.normalize :as thn]
|
||||
@ -23,11 +24,14 @@
|
||||
|
||||
(defn- live-model
|
||||
[state live]
|
||||
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/forest live))]
|
||||
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/forest live))
|
||||
ids-map (into {} (keep (fn [{:keys [path shape]}]
|
||||
(when-let [id (get by-path path)] [(:id shape) id])))
|
||||
(cg/forest (:situation state)))]
|
||||
(into {}
|
||||
(keep (fn [{:keys [path shape]}]
|
||||
(when-let [groups (get (:model state) (:id shape))]
|
||||
(when-let [id (get by-path path)] [id groups]))))
|
||||
(when-let [id (get by-path path)] [id (grid/remap-groups groups ids-map)]))))
|
||||
(cg/forest (:situation state)))))
|
||||
|
||||
(defn- ^:async replay-program
|
||||
@ -61,6 +65,10 @@
|
||||
(:objects (cthf/current-page (tm/file next-live)))
|
||||
(tm/shape-by-id next-live (get-in actual [:shape :id]))
|
||||
(cg/requested-geometry live actual (first tuple))))
|
||||
(when (and (:op actual) (#{:grid-cell :grid-direction} (first tuple)))
|
||||
(oracle/check-requested-grid
|
||||
(tm/shape-by-id next-live (get-in actual [:shape :id])) (:shape actual)
|
||||
(get-in actual [:op :action]) (get-in actual [:op :value])))
|
||||
(try
|
||||
(oracle/check next-live (live-model next-state next-live))
|
||||
(catch :default error
|
||||
@ -88,6 +96,22 @@
|
||||
rx/timer mock/timer-mock}
|
||||
(await (replay-program program)))))
|
||||
|
||||
(t/deftest ^:async authored-grid-histories-use-independent-cell-expectations
|
||||
(doseq [ops [[[:grid-direction 0 0 1 1] [:grid-direction 0 0 1 0]
|
||||
[:grid-direction 0 0 0 0] [:update-main 0]
|
||||
[:grid-direction 0 0 0 1] [:reset 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:grid-cell 0 1 1 0] [:grid-cell 0 0 2 0]
|
||||
[:update-main 0] [:grid-cell 0 1 2 1] [:reset 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:duplicate 0] [:undo] [:redo] [:update-main 0]]
|
||||
[[:grid-direction 0 0 1 1] [:add-child 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:remove-child 0] [:undo] [:redo]]
|
||||
[[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]]]
|
||||
(await (replay {:world [1 0 {:layout :grid :layout-children true :grid-square true}] :ops ops}))))
|
||||
|
||||
(t/deftest ^:async detaching-an-outer-copy-remaps-inherited-grid-cell-intent
|
||||
(await (replay {:world [1 1 {:layout :grid :layout-children true :grid-square true}]
|
||||
:ops [[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]})))
|
||||
|
||||
(t/deftest ^:async layout-container-histories-survive-component-operations
|
||||
(doseq [[property layouts] [[20 [:flex]] [21 [:flex]] [22 [:flex :grid]] [23 [:flex :grid]]
|
||||
[24 [:flex :grid]] [25 [:flex :grid]] [26 [:grid]]]
|
||||
|
||||
@ -140,6 +140,16 @@
|
||||
built from `situation` and from the same helpers the common node uses."
|
||||
[op situation]
|
||||
(cond
|
||||
(instance? n/EditGrid op)
|
||||
(let [{:keys [target action value]} op
|
||||
id (tm/target-shape-id situation target)
|
||||
shape (tm/shape-by-id situation id)]
|
||||
(case action
|
||||
:direction [(dwsl/change-grid-direction [id] value)]
|
||||
:cell (let [cell (first (filter #(= [(:row value) (:column value)] [(:row %) (:column %)])
|
||||
(vals (:layout-grid-cells shape))))]
|
||||
[(dwsl/update-grid-cells id [(:id cell)] (:props value))])))
|
||||
|
||||
(instance? n/ChangeProperty op)
|
||||
(let [{:keys [target property value]} op
|
||||
id (tm/target-shape-id situation target)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user