0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-10 09:08:31 -05:00

Merge pull request #501 from penpot/more-tests

🎉 Add frontend tests for creating and renaming components
This commit is contained in:
Andrey Antukh 2021-01-29 22:53:20 +01:00 committed by GitHub
commit b57e63d7d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 409 additions and 148 deletions

View file

@ -25,7 +25,7 @@
(= (count shapes) 1)
(= (:type (first shapes)) :group))
(:name (first shapes))
(name (gensym prefix)))]
(name (gensym prefix)))] ; TODO: we should something like in new shapes
(-> (cp/make-minimal-group frame-id selrect group-name)
(gsh/setup selrect)
(assoc :shapes (mapv :id shapes)))))

View file

@ -17,7 +17,6 @@
[app.common.geom.shapes :as geom]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries-helpers :as dwlh]
[app.common.pages :as cp]
[app.main.repo :as rp]
@ -224,87 +223,22 @@
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(def add-component
"Add a new component to current file library, from the currently selected shapes"
"Add a new component to current file library, from the currently selected shapes."
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state stream]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
shapes (dwg/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; we need to create a group before creating the component.
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dwg/prepare-create-group page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(dwlh/make-component-shape group objects file-id)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val (:component-file updated-shape)}
{:type :set
:attr :component-root?
:val (:component-root? updated-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
(let [original-shape (get objects (:id updated-shape))]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id original-shape)}
{:type :set
:attr :component-file
:val (:component-file original-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref original-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
selected (get-in state [:workspace-local :selected])]
(let [[group rchanges uchanges]
(dwlh/generate-add-component selected objects page-id file-id)]
(when-not (empty? rchanges)
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(defn rename-component
"Rename the component with the given id, in the current file library."
[id new-name]
(us/assert ::us/uuid id)
(us/assert ::us/string new-name)

View file

@ -16,6 +16,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.pages :as cp]
[app.main.data.workspace.groups :as dwg]
[app.util.logging :as log]
[app.util.text :as ut]))
@ -122,6 +123,78 @@
(cp/clone-object shape nil objects update-new-shape update-original-shape)))
(defn generate-add-component
"If there is exactly one id, and it's a group, use it as root. Otherwise,
create a group that contains all ids. Then, make a component with it,
and link all shapes to their corresponding one in the component."
[ids objects page-id file-id]
(let [shapes (dwg/shapes-for-grouping objects ids)
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dwg/prepare-create-group page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(make-component-shape group objects file-id)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val (:component-file updated-shape)}
{:type :set
:attr :component-root?
:val (:component-root? updated-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
(let [original-shape (get objects (:id updated-shape))]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id original-shape)}
{:type :set
:attr :component-file
:val (:component-file original-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref original-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
[group rchanges uchanges]))
(defn duplicate-component
"Clone the root shape of the component and all children. Generate new
ids from all of them."

View file

@ -0,0 +1,39 @@
(ns app.test-helpers.events
(:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.uuid :as uuid]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]))
;; ---- Helpers to manage global events
(defn do-update
"Execute an update event and returns the new state."
[event state]
(ptk/update event state))
(defn do-watch
"Execute a watch event and return an observable, that
emits once a list with all new events."
[event state]
(->> (ptk/watch event state nil)
(rx/reduce conj [])))
(defn do-watch-update
"Execute a watch event and return an observable, that
emits once the new state, after all new events applied
in sequence (considering they are all update events)."
[event state]
(->> (do-watch event state)
(rx/map (fn [new-events]
(reduce
(fn [new-state new-event]
(do-update new-event new-state))
state
new-events)))))

View file

@ -0,0 +1,90 @@
(ns app.test-helpers.libraries
(:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.uuid :as uuid]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries-helpers :as dwlh]
[app.test-helpers.pages :as thp]))
;; ---- Helpers to manage libraries and synchronization
(defn is-instance-root
[shape]
(t/is (some? (:shape-ref shape)))
(t/is (some? (:component-id shape)))
(t/is (= (:component-root? shape) true)))
(defn is-instance-subroot
[shape]
(t/is (some? (:shape-ref shape)))
(t/is (some? (:component-id shape)))
(t/is (nil? (:component-root? shape))))
(defn is-instance-head
[shape]
(t/is (some? (:shape-ref shape)))
(t/is (some? (:component-id shape))))
(defn is-instance-child
[shape]
(t/is (some? (:shape-ref shape)))
(t/is (nil? (:component-id shape)))
(t/is (nil? (:component-file shape)))
(t/is (nil? (:component-root? shape))))
(defn is-noninstance
[shape]
(t/is (nil? (:shape-ref shape)))
(t/is (nil? (:component-id shape)))
(t/is (nil? (:component-file shape)))
(t/is (nil? (:component-root? shape)))
(t/is (nil? (:remote-synced? shape)))
(t/is (nil? (:touched shape))))
(defn is-from-file
[shape file]
(t/is (= (:component-file shape)
(:id file))))
(defn resolve-instance-and-master
[state root-inst-id]
(let [page (thp/current-page state)
root-inst (cph/get-shape page root-inst-id)
file (dwlh/get-local-file state)
component (cph/get-component
(:component-id root-inst)
(:id file)
file
nil)
shapes-inst (cph/get-object-with-children
root-inst-id
(:objects page))
shapes-master (cph/get-object-with-children
(:shape-ref root-inst)
(:objects component))
unique-refs (into #{} (map :shape-ref shapes-inst))
master-exists? (fn [shape]
(t/is (some #(= (:id %) (:shape-ref shape))
shapes-master)))]
;; Validate that the instance tree is well constructed
(t/is (is-instance-root (first shapes-inst)))
(run! is-instance-child (rest shapes-inst))
(run! is-noninstance shapes-master)
(t/is (= (count shapes-inst)
(count shapes-master)
(count unique-refs)))
(run! master-exists? shapes-inst)
[shapes-inst shapes-master component]))

View file

@ -1,4 +1,4 @@
(ns app.test-helpers
(ns app.test-helpers.pages
(:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[beicon.core :as rx]
@ -8,36 +8,9 @@
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]))
;; ---- Helpers to manage global events
(defn do-update
"Execute an update event and returns the new state."
[event state]
(ptk/update event state))
(defn do-watch
"Execute a watch event and return an observable, that
emits once a list with all new events."
[event state]
(->> (ptk/watch event state nil)
(rx/reduce conj [])))
(defn do-watch-update
"Execute a watch event and return an observable, that
emits once the new state, after all new events applied
in sequence (considering they are all update events)."
[event state]
(->> (do-watch event state)
(rx/map (fn [new-events]
(reduce
(fn [new-state new-event]
(do-update new-event new-state))
state
new-events)))))
[app.main.data.workspace :as dw]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries-helpers :as dwlh]))
;; ---- Helpers to manage pages and objects
@ -53,16 +26,32 @@
:pages-index {}}
:workspace-libraries {}})
(def ^:private idmap (atom {}))
(defn reset-idmap! []
(reset! idmap {}))
(defn current-page
[state]
(let [page-id (:current-page-id state)]
(get-in state [:workspace-data :pages-index page-id])))
(defn id
[label]
(get @idmap label))
(defn get-shape
[state label]
(let [page (current-page state)]
(get-in page [:objects (id label)])))
(defn sample-page
([state] (sample-page state {}))
([state {:keys [id name] :as props
:or {id (uuid/next)
name "page1"}}]
(swap! idmap assoc :page id)
(-> state
(assoc :current-page-id id)
(update :workspace-data
@ -72,13 +61,14 @@
:name name}]))))
(defn sample-shape
([state type] (sample-shape state type {}))
([state type props]
([state label type] (sample-shape state type {}))
([state label type props]
(let [page (current-page state)
frame (cph/get-top-frame (:objects page))
shape (-> (cp/make-minimal-shape type)
(gsh/setup {:x 0 :y 0 :width 1 :height 1})
(merge props))]
(swap! idmap assoc label (:id shape))
(update state :workspace-data
cp/process-changes
[{:type :add-obj
@ -87,3 +77,30 @@
:frame-id (:id frame)
:obj shape}]))))
(defn group-shapes
([state label ids] (group-shapes state label ids "Group-"))
([state label ids prefix]
(let [page (current-page state)
shapes (dwg/shapes-for-grouping (:objects page) ids)
[group rchanges uchanges]
(dwg/prepare-create-group (:id page) shapes prefix true)]
(swap! idmap assoc label (:id group))
(update state :workspace-data
cp/process-changes rchanges))))
(defn make-component
[state label ids]
(let [page (current-page state)
[group rchanges uchanges]
(dwlh/generate-add-component ids
(:objects page)
(:id page)
current-file-id)]
(swap! idmap assoc label (:id group))
(update state :workspace-data
cp/process-changes rchanges)))

View file

@ -2,37 +2,40 @@
(:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[beicon.core :as rx]
[app.test-helpers :as th]
[linked.core :as lks]
[app.test-helpers.events :as the]
[app.test-helpers.pages :as thp]
[app.test-helpers.libraries :as thl]
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.libraries-helpers :as dwlh]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
(t/deftest test-create-page
(t/testing "create page"
(let [state (-> th/initial-state
(th/sample-page))
page (th/current-page state)]
(let [state (-> thp/initial-state
(thp/sample-page))
page (thp/current-page state)]
(t/is (= (:name page) "page1")))))
(t/deftest test-create-shape
(t/testing "create shape"
(let [id (uuid/next)
state (-> th/initial-state
(th/sample-page)
(th/sample-shape :rect {:id id
:name "Rect 1"}))
page (th/current-page state)
shape (cph/get-shape page id)]
(let [state (-> thp/initial-state
(thp/sample-page)
(thp/sample-shape :shape1 :rect
{:name "Rect 1"}))
shape (thp/get-shape state :shape1)]
(t/is (= (:name shape) "Rect 1")))))
(t/deftest synctest
(t/testing "synctest"
(let [state {:workspace-local {:color-for-rename "something"}}
new-state (->> state
(th/do-update
(the/do-update
dwl/clear-color-for-rename))]
(t/is (= (get-in new-state [:workspace-local :color-for-rename])
nil)))))
@ -43,7 +46,7 @@
(let [state {}
color {:color "#ffffff"}]
(->> state
(th/do-watch-update
(the/do-watch-update
(dwl/add-recent-color color))
(rx/map
(fn [new-state]
@ -56,43 +59,148 @@
[color]))))
(rx/subs done))))))
(t/deftest test-add-component
(t/testing "Add a component"
(t/deftest test-add-component-from-single-shape
(t/testing "Add a component from a single shape"
(t/async done
(let [id1 (uuid/next)
state (-> th/initial-state
(th/sample-page)
(th/sample-shape :rect
{:id id1
:name "Rect 1"}))]
(let [state (-> thp/initial-state
(thp/sample-page)
(thp/sample-shape :shape1 :rect
{:name "Rect 1"}))]
(->> state
(th/do-update (dw/select-shape id1))
(th/do-watch-update dwl/add-component)
(the/do-update (dw/select-shape (thp/id :shape1)))
(the/do-watch-update dwl/add-component)
(rx/map
(fn [new-state]
(let [page (th/current-page new-state)
shape (cph/get-shape page id1)
group (cph/get-shape page (:parent-id shape))
(let [shape1 (thp/get-shape new-state :shape1)
component (cph/get-component
(:component-id group)
(:current-file-id new-state)
(dwlh/get-local-file new-state)
nil)
[[group shape1] [c-group c-shape1] component]
(thl/resolve-instance-and-master
new-state
(:parent-id shape1))
c-shape (cph/get-shape
component
(:shape-ref shape))
file (dwlh/get-local-file new-state)]
c-group (cph/get-shape
component
(:shape-ref group))]
(t/is (= (:name shape) "Rect 1"))
(t/is (= (:name shape1) "Rect 1"))
(t/is (= (:name group) "Component-1"))
(t/is (= (:name component) "Component-1"))
(t/is (= (:name c-shape) "Rect 1"))
(t/is (= (:name c-group) "Component-1")))))
(t/is (= (:name c-shape1) "Rect 1"))
(t/is (= (:name c-group) "Component-1"))
(thl/is-from-file group file))))
(rx/subs done))))))
(t/deftest test-add-component-from-several-shapes
(t/testing "Add a component from several shapes"
(t/async done
(let [state (-> thp/initial-state
(thp/sample-page)
(thp/sample-shape :shape1 :rect
{:name "Rect 1"})
(thp/sample-shape :shape2 :rect
{:name "Rect 2"}))]
(->> state
(the/do-update (dw/select-shapes (lks/set
(thp/id :shape1)
(thp/id :shape2))))
(the/do-watch-update dwl/add-component)
(rx/map
(fn [new-state]
(let [shape1 (thp/get-shape new-state :shape1)
[[group shape1 shape2]
[c-group c-shape1 c-shape2]
component]
(thl/resolve-instance-and-master
new-state
(:parent-id shape1))
file (dwlh/get-local-file new-state)]
;; NOTE: the group name depends on having executed
;; the previous test.
(t/is (= (:name group) "Component-2"))
(t/is (= (:name shape1) "Rect 1"))
(t/is (= (:name shape2) "Rect 2"))
(t/is (= (:name component) "Component-2"))
(t/is (= (:name c-group) "Component-2"))
(t/is (= (:name c-shape1) "Rect 1"))
(t/is (= (:name c-shape2) "Rect 2"))
(thl/is-from-file group file))))
(rx/subs done))))))
(t/deftest test-add-component-from-group
(t/testing "Add a component from a group"
(t/async done
(let [
state (-> thp/initial-state
(thp/sample-page)
(thp/sample-shape :shape1 :rect
{:name "Rect 1"})
(thp/sample-shape :shape2 :rect
{:name "Rect 2"})
(thp/group-shapes :group1
[(thp/id :shape1)
(thp/id :shape2)]))]
(->> state
(the/do-update (dw/select-shape (thp/id :group1)))
(the/do-watch-update dwl/add-component)
(rx/map
(fn [new-state]
(let [[[group shape1 shape2]
[c-group c-shape1 c-shape2]
component]
(thl/resolve-instance-and-master
new-state
(thp/id :group1))
file (dwlh/get-local-file new-state)]
(t/is (= (:name shape1) "Rect 1"))
(t/is (= (:name shape2) "Rect 2"))
(t/is (= (:name group) "Group-3"))
(t/is (= (:name component) "Group-3"))
(t/is (= (:name c-shape1) "Rect 1"))
(t/is (= (:name c-shape2) "Rect 2"))
(t/is (= (:name c-group) "Group-3"))
(thl/is-from-file group file))))
(rx/subs done))))))
(t/deftest test-rename-component
(t/testing "Rename a component"
(t/async done
(let [
state (-> thp/initial-state
(thp/sample-page)
(thp/sample-shape :shape1 :rect
{:name "Rect 1"})
(thp/make-component :instance1
[(thp/id :shape1)]))
instance1 (thp/get-shape state :instance1)]
(->> state
(the/do-watch-update (dwl/rename-component
(:component-id instance1)
"Renamed component"))
(rx/map
(fn [new-state]
(let [file (dwlh/get-local-file new-state)
component (cph/get-component
(:component-id instance1)
(:component-file instance1)
file
{})]
(t/is (= (:name component)
"Renamed component")))))
(rx/subs done))))))