mirror of
https://github.com/penpot/penpot.git
synced 2026-11-02 16:10:42 +00:00
Fixes in the variant events of the workspace and in the component calls of the plugin API (Phase 3c). - Rename a variant in one commit. Renaming a variant renamed its board, then each of its components and mains, as separate commits inside an undo transaction. The transaction kept the undo group of its first commit, the board's, while the sync that the renamed mains started took the group of their commits and made an entry of its own: one undo reverted only the copies' names (F6, case AV). clv/generate-rename-variant builds the board, component and main renames in common, and the event commits them once, so the rename and its sync are one undo entry, like transform into a variant. Case AV no longer marks its round trip as a known failure; the frontend golden master records it without failures. - Keep overrides in a plugin swap only within a variant set. The plugin's swapComponent swapped with keep-touched for any component, while the UI keeps a copy's overrides only on a variant switch: the same swap kept different overrides by route (F33). It now keeps them only when the copy's component and the target are variants of one set (decision (p)), as swap-keeps-touched? says. shape-bugfixes-test: two variants of one set keep the overrides; of different sets, or plain components, do not. - Look up a plugin variant head's component in its own file. The plugin's isVariantHead looked the shape's component up in the file of the shape, so it was false for copies of a library's variants; and it looked it up before checking the shape is a head, so on a shape with no component it failed the lookup's assert (F91). It now checks the head first and looks in the shape's :component-file. shape-bugfixes-test: a copy of a library variant is a variant head, and a plain rect is not. - Add a variant with the shortcut when a variant set is selected. The context menu of a variant container offers "Add variant" and shows the create-component shortcut for it, but the shortcut's event did not know containers: with one selected it fell through to creating a component, which the container's own rules refuse (F95). With a single variant container selected, the shortcut now adds a variant to it, as the menu entry does. workspace-variants-test: the shortcut on a variant container gives the set a third member. - Refuse plugin variant edits whose variant is on another page. The variant events the plugin API calls (add a variant, add, remove or rename a property, set a property value) build their changes on the objects of the page the workspace shows. For a variant whose mains sit on another page, they updated the components but not their mains, so the mains kept their old :variant-name and the file failed validation (F94). The plugin now refuses these edits, as it refuses edits of shapes on another page, unless the variant is in the current file and on the current page. library-test: each edit goes through with the variant on the current page and is refused, with nothing emitted, with it elsewhere. - Check every component of an assets group before combining it. The assets panel offers to combine a group of components as variants when they can be: more than one, none a variant, all with their main on one page. It counted every component of the group, subgroups included, but checked the other two rules only on the components directly in it, while the combine takes them all; with none directly in it, those rules always held (F98). can-combine-group? now checks every component of the group. assets-components-test: a group whose only components are in a subgroup can be combined; a variant or a main on another page in a subgroup blocks it; one component cannot. AI-assisted-by: claude-opus-5-5
263 lines
11 KiB
Clojure
263 lines
11 KiB
Clojure
;; 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-variants-test
|
|
"The checks the workspace runs before combining components as variants
|
|
and before a variant switch, and the branches of the variant events that
|
|
the composable cases (`frontend-tests.composable-tests.comp.sync-test`)
|
|
do not reach: a combine on another page, a switch that would nest a
|
|
component in itself, the shortcuts with no variant selected and the
|
|
rename of a plain component."
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.logic.variants :as clv]
|
|
[app.common.test-helpers.components :as cthc]
|
|
[app.common.test-helpers.compositions :as ctho]
|
|
[app.common.test-helpers.files :as cthf]
|
|
[app.common.test-helpers.ids-map :as cthi]
|
|
[app.common.test-helpers.shapes :as cths]
|
|
[app.common.test-helpers.variants :as cthv]
|
|
[app.common.types.components-list :as ctkl]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.common :as dcm]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.main.data.workspace.variants :as dwv]
|
|
[app.util.dom :as dom]
|
|
[beicon.v2.core :as rx]
|
|
[cljs.test :as t :include-macros true]
|
|
[frontend-tests.helpers.mock :as mock]
|
|
[frontend-tests.helpers.state :as ths]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(t/use-fixtures :each {:before cthi/reset-idmap!})
|
|
|
|
(defn- setup-file
|
|
"A variant set (members :m01 and :m02, each with a child), a copy of :m01,
|
|
two plain components and a copy of the first."
|
|
[]
|
|
(-> (cthf/sample-file :file1)
|
|
(cthv/add-variant-with-child :v01 :c01 :m01 :c02 :m02 :m01-child :m02-child)
|
|
(cthc/instantiate-component :c01 :copy01 :children-labels [:copy01-child])
|
|
(ctho/add-simple-component :plain1 :plain1-root :plain1-child)
|
|
(ctho/add-simple-component :plain2 :plain2-root :plain2-child)
|
|
(cthc/instantiate-component :plain1 :plain1-copy)))
|
|
|
|
(defn- state-of
|
|
[file]
|
|
@(ths/setup-store file))
|
|
|
|
(t/deftest valid-components-for-variants
|
|
(let [file (setup-file)
|
|
state (state-of file)
|
|
page-id (:current-page-id state)
|
|
valid? #(dwv/valid-components-for-variants? state page-id (mapv cthi/id %))]
|
|
(t/testing "two plain main roots"
|
|
(t/is (valid? [:plain1-root :plain2-root])))
|
|
(t/testing "fewer than two distinct components"
|
|
(t/is (not (valid? [:plain1-root])))
|
|
(t/is (not (valid? [:plain1-root :plain1-root]))))
|
|
(t/testing "a variant main"
|
|
(t/is (not (valid? [:plain1-root :m01]))))
|
|
(t/testing "a copy"
|
|
(t/is (not (valid? [:plain1-root :plain1-copy]))))
|
|
(t/testing "a page that is not the current one"
|
|
(t/is (not (dwv/valid-components-for-variants?
|
|
state (uuid/next) [(cthi/id :plain1-root) (cthi/id :plain2-root)]))))))
|
|
|
|
(t/deftest valid-variant-switch
|
|
(let [file (setup-file)
|
|
state (state-of file)
|
|
valid? (fn [label pos val]
|
|
(clv/valid-switch? (dsh/lookup-libraries state) (cths/get-shape file label) pos val))]
|
|
(t/testing "a variant copy head and a value some member holds"
|
|
(t/is (valid? :copy01 0 "Value2"))
|
|
(t/is (valid? :copy01 0 "Value1")))
|
|
(t/testing "a value no member holds"
|
|
(t/is (not (valid? :copy01 0 "Value3"))))
|
|
(t/testing "a position outside the properties"
|
|
(t/is (not (valid? :copy01 1 "Value2")))
|
|
(t/is (not (valid? :copy01 -1 "Value2"))))
|
|
(t/testing "a value that is not a string"
|
|
(t/is (not (valid? :copy01 0 2))))
|
|
(t/testing "not a variant copy head"
|
|
(t/is (not (valid? :m01 0 "Value2")))
|
|
(t/is (not (valid? :copy01-child 0 "Value2")))
|
|
(t/is (not (valid? :plain1-copy 0 "Value2"))))))
|
|
|
|
(defn- all-transactions-committed
|
|
"A `run-store` stopper: the first undo commit after which no transaction
|
|
is pending, for events that commit their transaction asynchronously."
|
|
[store]
|
|
(fn [stream]
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dwu/commit-undo-transaction))
|
|
(rx/filter #(empty? (get-in @store [:workspace-undo :transactions-pending]))))))
|
|
|
|
(defn- go-to-page
|
|
"Stands in for `dcm/go-to-workspace`, which needs the router: makes
|
|
`page-id` the current page and announces it as initialized."
|
|
[& {:keys [page-id]}]
|
|
(ptk/reify ::go-to-page
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc state :current-page-id page-id))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (ptk/data-event :app.main.data.workspace.pages/initialize-page {})))))
|
|
|
|
(t/deftest combine-on-another-page
|
|
(t/async
|
|
done
|
|
(mock/with-mocks
|
|
{dcm/go-to-workspace go-to-page}
|
|
(fn [inner-done]
|
|
(let [file (cthf/sample-file :file1)
|
|
page-1 (cthf/current-page-id file)
|
|
file (-> file
|
|
(cthf/add-sample-page :page2)
|
|
(ctho/add-simple-component :plain1 :plain1-root :plain1-child)
|
|
(ctho/add-simple-component :plain2 :plain2-root :plain2-child)
|
|
(vary-meta assoc :current-page-id page-1))
|
|
store (ths/setup-store file)
|
|
ids [(cthi/id :plain1-root) (cthi/id :plain2-root)]]
|
|
(ths/run-store
|
|
store inner-done
|
|
[(dwv/combine-as-variants ids {:page-id (cthi/id :page2)
|
|
:trigger "plugin:combine-as-variants"})]
|
|
(fn [state]
|
|
(let [data (:data (ths/get-file-from-state state))
|
|
c1 (ctkl/get-component data (cthi/id :plain1))
|
|
c2 (ctkl/get-component data (cthi/id :plain2))]
|
|
(t/is (= (cthi/id :page2) (:current-page-id state)))
|
|
(t/is (some? (:variant-id c1)))
|
|
(t/is (= (:variant-id c1) (:variant-id c2)))))
|
|
(all-transactions-committed store))))
|
|
done)))
|
|
|
|
(t/deftest switch-into-a-nesting-loop
|
|
(t/async
|
|
done
|
|
(let [file (-> (cthf/sample-file :file1)
|
|
(cthv/add-variant :v01 :c01 :m01 :c02 :m02)
|
|
(cthc/instantiate-component :c01 :nested :parent-label :m02))
|
|
store (ths/setup-store file)
|
|
errors (atom 0)
|
|
params (with-meta {:shapes [(cths/get-shape file :nested)] :pos 0 :val "Value2"}
|
|
{:on-error #(swap! errors inc)})]
|
|
(ths/run-store
|
|
store done
|
|
[(dwv/variants-switch params)]
|
|
(fn [state]
|
|
(t/is (= 1 @errors))
|
|
(t/is (= (cthi/id :c01)
|
|
(:component-id (cths/get-shape (ths/get-file-from-state state) :nested)))))))))
|
|
|
|
(defn- object-count
|
|
[file]
|
|
(count (:objects (cthf/current-page file))))
|
|
|
|
(defn- run-selected
|
|
"Run `event` on a store for `file` with the shapes `labels` selected, then
|
|
`check` the resulting file."
|
|
[done file labels event check]
|
|
(ths/run-store
|
|
(ths/setup-store file) done
|
|
[(dws/select-shapes (into (d/ordered-set) (map cthi/id) labels)) event]
|
|
#(check (ths/get-file-from-state %))))
|
|
|
|
(defn- file-with-a-rect
|
|
[]
|
|
(-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :rect)))
|
|
|
|
(t/deftest duplicate-shortcut-with-nothing-selected
|
|
(t/async
|
|
done
|
|
(let [file (file-with-a-rect)]
|
|
(run-selected done file [] (dwv/duplicate-or-add-variant)
|
|
#(t/is (= (object-count file) (object-count %)))))))
|
|
|
|
(t/deftest duplicate-shortcut-on-a-plain-shape
|
|
(t/async
|
|
done
|
|
(let [file (file-with-a-rect)]
|
|
(run-selected done file [:rect] (dwv/duplicate-or-add-variant)
|
|
#(t/is (= (inc (object-count file)) (object-count %)))))))
|
|
|
|
(t/deftest create-component-shortcut-on-a-plain-shape
|
|
(t/async
|
|
done
|
|
(run-selected done (file-with-a-rect) [:rect] (dwv/add-component-or-variant)
|
|
#(t/is (= 1 (count (ctkl/components-seq (:data %))))))))
|
|
|
|
(t/deftest create-component-shortcut-on-a-variant-container
|
|
;; the shortcut the container's "Add variant" menu entry shows; the new
|
|
;; variant focuses its first property 250 ms later, which finds no DOM here
|
|
(t/async
|
|
done
|
|
(mock/with-mocks
|
|
{dom/get-element (constantly nil)}
|
|
(fn [inner-done]
|
|
(let [file (-> (cthf/sample-file :file1)
|
|
(cthv/add-variant :v01 :c01 :m01 :c02 :m02))]
|
|
(run-selected #(js/setTimeout inner-done 300) file [:v01] (dwv/add-component-or-variant)
|
|
(fn [file']
|
|
(t/is (= 3 (count (filter #(= (cthi/id :v01) (:variant-id %))
|
|
(ctkl/components-seq (:data file'))))))))))
|
|
done)))
|
|
|
|
(t/deftest rename-of-a-plain-component
|
|
(t/async
|
|
done
|
|
(let [file (-> (cthf/sample-file :file1)
|
|
(ctho/add-simple-component :plain1 :plain1-root :plain1-child))]
|
|
(ths/run-store
|
|
(ths/setup-store file) done
|
|
[(dwv/rename-comp-or-variant-and-main (cthi/id :plain1) "Renamed")]
|
|
(fn [state]
|
|
(let [file' (ths/get-file-from-state state)]
|
|
(t/is (= "Renamed" (:name (ctkl/get-component (:data file') (cthi/id :plain1)))))
|
|
(t/is (= "Renamed" (:name (cths/get-shape file' :plain1-root))))))))))
|
|
|
|
(t/deftest remove-empty-properties-of-a-set-with-many-properties
|
|
(t/async
|
|
done
|
|
(let [props (fn [value]
|
|
(mapv (fn [i]
|
|
{:name (str "Property " (inc i))
|
|
:value (if (= i 5) "" value)})
|
|
(range 10)))
|
|
file (-> (cthf/sample-file :file1)
|
|
(cthv/add-variant :v01 :c01 :m01 :c02 :m02)
|
|
(cthc/update-component :c01 {:variant-properties (props "a")})
|
|
(cthc/update-component :c02 {:variant-properties (props "b")}))]
|
|
(ths/run-store
|
|
(ths/setup-store file) done
|
|
[(dwv/remove-empty-properties (cthi/id :v01))]
|
|
(fn [state]
|
|
(let [data (:data (ths/get-file-from-state state))]
|
|
(t/is (= (remove #{"Property 6"} (map :name (props "a")))
|
|
(map :name (:variant-properties (ctkl/get-component data (cthi/id :c01))))))))))))
|
|
|
|
(t/deftest set-a-property-value-on-several-members
|
|
(t/async
|
|
done
|
|
(let [file (-> (setup-file)
|
|
(cths/update-shape :m01 :variant-error "Duplicated")
|
|
(cths/update-shape :m02 :variant-error "Duplicated"))]
|
|
(ths/run-store
|
|
(ths/setup-store file) done
|
|
[(dwv/update-property-value [(cthi/id :c01) (cthi/id :c02)] 0 "Same")]
|
|
(fn [state]
|
|
(let [file' (ths/get-file-from-state state)
|
|
value #(get-in (ctkl/get-component (:data file') (cthi/id %)) [:variant-properties 0 :value])]
|
|
(t/is (= ["Same" "Same"] [(value :c01) (value :c02)]))
|
|
(t/is (nil? (:variant-error (cths/get-shape file' :m01))))
|
|
(t/is (nil? (:variant-error (cths/get-shape file' :m02))))))))))
|