penpot/frontend/test/frontend_tests/data/workspace_variants_test.cljs

559 lines
24 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
(:require
[app.common.files.changes-builder :as pcb]
[app.common.files.variant :as cfv]
[app.common.logic.variants :as clv]
[app.common.path-names :as cpn]
[app.common.test-helpers.components :as cthc]
[app.common.test-helpers.compositions :as tho]
[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 thv]
[app.common.types.component :as ctk]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.variants :as dwv]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
[potok.v2.core :as ptk]))
(t/use-fixtures :each
{:before cthi/reset-idmap!})
(defn- two-property-file-on-other-page
[]
(-> (cthf/sample-file :file1 :page-label :page1)
(thv/add-variant-two-properties :v01 :c01 :m01 :c02 :m02)
(cthf/add-sample-page :page2)))
(defn- run
[file events done assert-fn]
(ths/run-store
(ths/setup-store file)
done
events
(fn [state]
(cthf/validate-file! (ths/get-file-from-state state))
(assert-fn state))))
(defn- variant-name
[file label]
(:variant-name (cths/get-shape file label :page-label :page1)))
(t/deftest update-property-value-from-another-page
(t/async
done
(let [file (two-property-file-on-other-page)
component-id (:id (cthc/get-component file :c01))
other-id (cthi/id :page2)]
(run file
[(dwv/update-property-value component-id 0 "active")
(dwv/update-property-value component-id 1 "crítica")]
done
(fn [state]
(let [file (ths/get-file-from-state state)
props (:variant-properties (cthc/get-component file :c01))]
(t/is (= other-id (:current-page-id state)))
(t/is (= "crítica" (-> props second :value)))
(t/is (= "active, crítica" (variant-name file :m01)))
(t/is (= (ctv/properties-to-name props) (variant-name file :m01)))))))))
(t/deftest update-property-value-on-main-page
(t/async
done
(let [file (-> (two-property-file-on-other-page)
(cthf/switch-to-page :page1))
component-id (:id (cthc/get-component file :c01))]
(run file
[(dwv/update-property-value component-id 1 "crítica")]
done
(fn [state]
(let [file (ths/get-file-from-state state)
props (:variant-properties (cthc/get-component file :c01))]
(t/is (= (cthi/id :page1) (:current-page-id state)))
(t/is (= "crítica" (-> props second :value)))
(t/is (= (ctv/properties-to-name props) (variant-name file :m01)))))))))
(t/deftest add-remove-reorder-from-another-page
(t/async
done
(let [file (two-property-file-on-other-page)
variant-id (cthi/id :v01)]
(run file
[(dwv/add-new-property variant-id {:fill-values? true})
(dwv/reorder-variant-poperties variant-id 0 3)
(dwv/remove-property variant-id 0)]
done
(fn [state]
(let [file (ths/get-file-from-state state)
comp1 (cthc/get-component file :c01)
comp2 (cthc/get-component file :c02)
names1 (mapv :name (:variant-properties comp1))
names2 (mapv :name (:variant-properties comp2))]
(t/is (= names1 names2))
(t/is (= (ctv/properties-to-name (:variant-properties comp1))
(variant-name file :m01)))
(t/is (= (ctv/properties-to-name (:variant-properties comp2))
(variant-name file :m02)))))))))
(t/deftest formula-rename-from-another-page
(t/async
done
(let [file (two-property-file-on-other-page)
comp (cthc/get-component file :c01)
previous (:variant-properties comp)
updated [{:name "Property 1" :value "active"}
{:name "Property 2" :value "crítica"}]]
(run file
[(dwv/update-properties-names-and-values (:id comp) (:variant-id comp) previous updated)
(dwv/remove-empty-properties (:variant-id comp))]
done
(fn [state]
(let [file (ths/get-file-from-state state)
props (:variant-properties (cthc/get-component file :c01))]
(t/is (= "active" (-> props first :value)))
(t/is (= "crítica" (-> props second :value)))
(t/is (= (ctv/properties-to-name props) (variant-name file :m01)))))))))
(t/deftest update-property-name-from-another-page
(t/async
done
(let [file (two-property-file-on-other-page)
variant-id (cthi/id :v01)]
(run file
[(dwv/update-property-name variant-id 0 "Status" {:trigger "test"})]
done
(fn [state]
(let [file (ths/get-file-from-state state)
names (mapv :name (:variant-properties (cthc/get-component file :c01)))]
(t/is (= names (mapv :name (:variant-properties (cthc/get-component file :c02)))))
(t/is (= "Status" (first names)))))))))
(t/deftest rename-variant-from-another-page
(t/async
done
(let [file (two-property-file-on-other-page)
variant-id (cthi/id :v01)
other-id (cthi/id :page2)
clean-name (cpn/clean-path "Renamed set")]
(run file
[(dwv/rename-variant variant-id "Renamed set")]
done
(fn [state]
(let [file (ths/get-file-from-state state)
container (cths/get-shape file :v01 :page-label :page1)
main1 (cths/get-shape file :m01 :page-label :page1)
main2 (cths/get-shape file :m02 :page-label :page1)
comp1 (cthc/get-component file :c01)
comp2 (cthc/get-component file :c02)]
(t/is (= other-id (:current-page-id state)))
(t/is (= clean-name (:name container)))
(t/is (= clean-name (:name main1)))
(t/is (= clean-name (:name main2)))
(t/is (= clean-name (cpn/merge-path-item (:path comp1) (:name comp1))))
(t/is (= clean-name (cpn/merge-path-item (:path comp2) (:name comp2))))))))))
;; ---------------------------------------------------------------------------
;; Create-set / combine: every commit must be a valid file
;; ---------------------------------------------------------------------------
(def ^:private apply-local?
(ptk/type? :app.main.data.changes/apply-changes-localy))
(defn- simple-main-file
[]
(-> (cthf/sample-file :file1)
(tho/add-simple-component :c01 :m01 :r01)))
(defn- two-mains-file
[]
(-> (cthf/sample-file :file1)
(tho/add-simple-component :c01 :m01 :r01
:root-params {:name "Alpha" :x 0 :y 0 :width 100 :height 80})
(tho/add-simple-component :c02 :m02 :r02
:root-params {:name "Beta" :x 150 :y 0 :width 100 :height 80})))
(defn- two-mains-shared-prefix-file
[]
(-> (cthf/sample-file :file1)
(tho/add-simple-component :c01 :m01 :r01
:root-params {:name "UI / Alpha" :x 0 :y 0 :width 100 :height 80})
(tho/add-simple-component :c02 :m02 :r02
:root-params {:name "UI / Beta" :x 150 :y 0 :width 100 :height 80})))
(defn- path-named-main-file
[]
(-> (cthf/sample-file :file1)
(tho/add-simple-component :c01 :m01 :r01
:root-params {:name "Group / Button"})))
(defn- two-mains-on-page1-with-page2
"Two mains on :page1; the open page is :page2 (Assets combine while elsewhere)."
[]
(-> (cthf/sample-file :file1 :page-label :page1)
(tho/add-simple-component :c01 :m01 :r01
:root-params {:name "Alpha" :x 0 :y 0 :width 100 :height 80})
(tho/add-simple-component :c02 :m02 :r02
:root-params {:name "Beta" :x 150 :y 0 :width 100 :height 80})
(cthf/add-sample-page :page2)
(cthf/switch-to-page :page2)))
(defn- post-artboard-file
"A normal frame holding one main — the state after create-artboard-from-shapes
and before the container flag."
[]
(-> (cthf/sample-file :file1)
(tho/add-frame :v01 {:name "Set"})
(tho/add-simple-component :c01 :m01 :r01
:root-params {:parent-label :v01 :name "Set"})))
(defn- component-full-name
[component]
(cpn/merge-path-item (:path component) (:name component)))
(defn- page-objects
[file]
(:objects (cthf/current-page file)))
(defn- page-objects-on
[file page-label]
(:objects (cthf/get-page file page-label)))
(defn- commit-error
[file]
(try
(cthf/validate-file! file)
nil
(catch :default cause
(or (ex-message cause) (str cause)))))
(defn- watch-applied-commits
"Record the file after every local apply. Keep the subscription for the
whole run-store; assertions run in the completed callback."
[store snapshots]
(->> (ptk/input-stream store)
(rx/filter apply-local?)
(rx/subs! (fn [_]
(let [file (ths/get-file-from-state @store)]
(swap! snapshots conj {:file file
:error (commit-error file)}))))))
(defn- after-commits-idle
"Stop once applied commits have been idle. Combine schedules an async
undo commit after the main stream."
[stream]
(->> stream
(rx/filter apply-local?)
(rx/debounce 100)))
(defn- assert-valid-snapshots
[snapshots]
(doseq [{:keys [error]} snapshots]
(t/is (nil? error) error)))
(defn- first-flagged
[snapshots container-id]
(first (filter #(ctk/is-variant-container?
(get (page-objects (:file %)) container-id))
snapshots)))
(t/deftest define-variant-container-commit-validates
(let [file (post-artboard-file)
page (cthf/current-page file)
variant-id (cthi/id :v01)
main-id (cthi/id :m01)
component (cthc/get-component file :c01)
component-id (:id component)
name (:name (cths/get-shape file :v01))
changes (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page))
(pcb/with-library-data (:data file))
(clv/generate-define-variant-container
variant-id main-id component-id name))
file' (cthf/apply-changes file changes)
container' (cths/get-shape file' :v01)
main' (cths/get-shape file' :m01)
comp' (cthc/get-component file' :c01)]
(t/is (ctk/is-variant-container? container'))
(t/is (= [main-id] (:shapes container')))
(t/is (= variant-id (:variant-id main')))
(t/is (= variant-id (:variant-id comp')))
(t/is (some? (:variant-id comp')))
(t/is (= (ctv/properties-to-name (:variant-properties comp'))
(:variant-name main')))))
(t/deftest transform-in-variant-validates-after-every-commit
(t/async
done
(let [file (simple-main-file)
main-id (cthi/id :m01)
variant-id (uuid/next)
snapshots (atom [])
store (ths/setup-store file)]
(watch-applied-commits store snapshots)
(ths/run-store
store
done
[(dwv/transform-in-variant main-id variant-id nil [] false true true)]
(fn [state]
(let [snaps @snapshots
flagged (first-flagged snaps variant-id)
file' (ths/get-file-from-state state)
objects' (page-objects file')
container' (get objects' variant-id)
main' (get objects' main-id)
comp' (cthc/get-component file' :c01)
before-flag (take-while
#(not (ctk/is-variant-container?
(get (page-objects (:file %)) variant-id)))
snaps)]
(assert-valid-snapshots snaps)
(t/is (some? flagged) "a commit must set :is-variant-container")
(doseq [snap before-flag]
(t/is (not (ctk/is-variant-container?
(get (page-objects (:file snap)) variant-id)))))
(when flagged
(let [ffile (:file flagged)
fobjs (page-objects ffile)
fcont (get fobjs variant-id)
fmain (get fobjs main-id)
fcomp (cthc/get-component ffile :c01)]
(t/is (= [main-id] (:shapes fcont)))
(t/is (ctk/main-instance? fmain))
(t/is (= variant-id (:variant-id fmain)))
(t/is (= variant-id (:variant-id fcomp)))
(t/is (= (ctv/properties-to-name (:variant-properties fcomp))
(:variant-name fmain)))))
(t/is (ctk/is-variant-container? container'))
(t/is (= variant-id (:variant-id main')))
(t/is (= variant-id (:variant-id comp')))))
after-commits-idle))))
(t/deftest transform-in-variant-path-name-validates-after-every-commit
(t/async
done
(let [file (path-named-main-file)
main-id (cthi/id :m01)
variant-id (uuid/next)
snapshots (atom [])
store (ths/setup-store file)]
(watch-applied-commits store snapshots)
(ths/run-store
store
done
[(dwv/transform-in-variant main-id variant-id nil [] false true true)]
(fn [state]
(let [snaps @snapshots
flagged (first-flagged snaps variant-id)
file' (ths/get-file-from-state state)
objects' (page-objects file')
container' (get objects' variant-id)
main' (get objects' main-id)
comp' (cthc/get-component file' :c01)
before-flag (take-while
#(not (ctk/is-variant-container?
(get (page-objects (:file %)) variant-id)))
snaps)]
(assert-valid-snapshots snaps)
(t/is (some? flagged) "a commit must set :is-variant-container")
(doseq [snap before-flag]
(let [ffile (:file snap)
fmain (get (page-objects ffile) main-id)
fcomp (cthc/get-component ffile :c01)]
(t/is (= "Group / Button" (:name fmain)))
(t/is (= "Group / Button" (component-full-name fcomp)))))
(when flagged
(let [ffile (:file flagged)
fobjs (page-objects ffile)
fcont (get fobjs variant-id)
fmain (get fobjs main-id)
fcomp (cthc/get-component ffile :c01)]
(t/is (= "Group" (:name fcont) (:name fmain) (component-full-name fcomp)))
(t/is (= variant-id (:variant-id fmain) (:variant-id fcomp)))
(t/is (= (ctv/properties-to-name (:variant-properties fcomp))
(:variant-name fmain)))))
(t/is (ctk/is-variant-container? container'))
(t/is (= "Group" (:name container') (:name main') (component-full-name comp')))
(t/is (= variant-id (:variant-id main') (:variant-id comp')))))
after-commits-idle))))
(t/deftest add-new-variant-on-existing-set-still-validates
(t/async
done
(let [file (-> (two-property-file-on-other-page)
(cthf/switch-to-page :page1))
main-id (cthi/id :m01)
c01-before (cthc/get-component file :c01)
c02-before (cthc/get-component file :c02)
last-c01 (-> c01-before :variant-properties last)
last-c02 (-> c02-before :variant-properties last)]
(run file
[(dwv/add-new-variant main-id)]
done
(fn [state]
(let [file' (ths/get-file-from-state state)
container' (cths/get-shape file' :v01 :page-label :page1)
c01' (cthc/get-component file' :c01)
c02' (cthc/get-component file' :c02)
new-id (first (:shapes container'))
new-shape (cths/get-shape-by-id file' new-id :page-label :page1)
new-comp (cthc/get-component-by-id file' (:component-id new-shape))]
(t/is (= 3 (count (:shapes container'))))
(t/is (= last-c01 (-> c01' :variant-properties last)))
(t/is (= last-c02 (-> c02' :variant-properties last)))
(t/is (not= last-c01 (-> new-comp :variant-properties last)))
(t/is (= (ctv/properties-to-name (:variant-properties new-comp))
(:variant-name new-shape)))))))))
(t/deftest combine-as-variants-validates-after-every-commit
(t/async
done
(let [file (two-mains-file)
id1 (cthi/id :m01)
id2 (cthi/id :m02)
variant-id (uuid/next)
snapshots (atom [])
store (ths/setup-store file)]
(watch-applied-commits store snapshots)
(ths/run-store
store
done
[(dwv/combine-as-variants [id1 id2] {:variant-id variant-id :trigger "test"})]
(fn [state]
(let [snaps @snapshots
file' (ths/get-file-from-state state)
objects (page-objects file')
container (get objects variant-id)
m01' (get objects id1)
m02' (get objects id2)
c01' (cthc/get-component file' :c01)
c02' (cthc/get-component file' :c02)
names1 (mapv :name (:variant-properties c01'))
names2 (mapv :name (:variant-properties c02'))
ordered (mapv :main-instance-id
(cfv/find-variant-components (:data file') objects variant-id))
before-flag (take-while
#(not (ctk/is-variant-container?
(get (page-objects (:file %)) variant-id)))
snaps)
flagged (first-flagged snaps variant-id)]
(assert-valid-snapshots snaps)
(doseq [snap before-flag]
(let [ffile (:file snap)
fmain (get (page-objects ffile) id1)
fcomp (cthc/get-component ffile :c01)]
(t/is (= "Alpha" (:name fmain)))
(t/is (= "Alpha" (component-full-name fcomp)))))
(when flagged
(let [ffile (:file flagged)
fobjs (page-objects ffile)
fcont (get fobjs variant-id)
fmain (get fobjs id1)
fcomp (cthc/get-component ffile :c01)]
(t/is (= "Component" (:name fcont) (:name fmain) (component-full-name fcomp)))))
(doseq [{:keys [file]} snaps]
(let [objs (page-objects file)
cont (get objs variant-id)
kids (set (:shapes cont))]
(when (contains? kids id2)
(t/is (ctk/is-variant-container? cont)
"other mains must not enter the frame before it is a variant container"))))
(t/is (ctk/is-variant-container? container))
(t/is (= names1 names2))
(t/is (= [id1 id2] ordered))
(t/is (= variant-id (:variant-id m01')))
(t/is (= variant-id (:variant-id m02')))
(t/is (= variant-id (:parent-id m01')))
(t/is (= variant-id (:parent-id m02')))))
after-commits-idle))))
(t/deftest combine-as-variants-shared-prefix-validates-after-every-commit
(t/async
done
(let [file (two-mains-shared-prefix-file)
id1 (cthi/id :m01)
id2 (cthi/id :m02)
variant-id (uuid/next)
snapshots (atom [])
store (ths/setup-store file)]
(watch-applied-commits store snapshots)
(ths/run-store
store
done
[(dwv/combine-as-variants [id1 id2] {:variant-id variant-id :trigger "test"})]
(fn [state]
(let [snaps @snapshots
file' (ths/get-file-from-state state)
objects (page-objects file')
container (get objects variant-id)
m01' (get objects id1)
m02' (get objects id2)
c01' (cthc/get-component file' :c01)
c02' (cthc/get-component file' :c02)
flagged (first-flagged snaps variant-id)]
(assert-valid-snapshots snaps)
(doseq [{:keys [file]} snaps]
(let [objs (page-objects file)
cont (get objs variant-id)
kids (set (:shapes cont))]
(when (contains? kids id2)
(t/is (ctk/is-variant-container? cont)
"other mains must not enter the frame before it is a variant container"))))
(when flagged
(let [ffile (:file flagged)
fobjs (page-objects ffile)
fcont (get fobjs variant-id)
fmain (get fobjs id1)
fcomp (cthc/get-component ffile :c01)]
(t/is (= "UI" (:name fcont) (:name fmain) (component-full-name fcomp)))))
(t/is (ctk/is-variant-container? container))
(t/is (= "UI" (:name container) (:name m01') (:name m02')
(component-full-name c01') (component-full-name c02')))
(t/is (= variant-id (:variant-id m01') (:variant-id m02')
(:variant-id c01') (:variant-id c02')))
(t/is (= variant-id (:parent-id m01') (:parent-id m02')))))
after-commits-idle))))
(t/deftest combine-as-variants-from-another-page-validates-after-every-commit
(t/async
done
(let [file (two-mains-on-page1-with-page2)
id1 (cthi/id :m01)
id2 (cthi/id :m02)
page1 (cthi/id :page1)
variant-id (uuid/next)
snapshots (atom [])
store (ths/setup-store file)]
(watch-applied-commits store snapshots)
(ths/run-store
store
done
[(dwv/combine-as-variants [id1 id2] {:page-id page1 :variant-id variant-id :trigger "test"})
(dw/initialize-page (:id file) page1)]
(fn [state]
(let [snaps @snapshots
file' (ths/get-file-from-state state)
objects (page-objects-on file' :page1)
container (get objects variant-id)
m01' (get objects id1)
m02' (get objects id2)
c01' (cthc/get-component file' :c01)
c02' (cthc/get-component file' :c02)]
(assert-valid-snapshots snaps)
(t/is (ctk/is-variant-container? container))
(t/is (= variant-id (:variant-id m01')))
(t/is (= variant-id (:variant-id m02')))
(t/is (= variant-id (:variant-id c01')))
(t/is (= variant-id (:variant-id c02')))))
after-commits-idle))))