0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 18:48:37 -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) (= (count shapes) 1)
(= (:type (first shapes)) :group)) (= (:type (first shapes)) :group))
(:name (first shapes)) (: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) (-> (cp/make-minimal-group frame-id selrect group-name)
(gsh/setup selrect) (gsh/setup selrect)
(assoc :shapes (mapv :id shapes))))) (assoc :shapes (mapv :id shapes)))))

View file

@ -17,7 +17,6 @@
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries-helpers :as dwlh] [app.main.data.workspace.libraries-helpers :as dwlh]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.main.repo :as rp] [app.main.repo :as rp]
@ -224,87 +223,22 @@
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(def add-component (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/reify ::add-component
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [file-id (:current-file-id state) (let [file-id (:current-file-id state)
page-id (:current-page-id state) page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id) objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected]) selected (get-in state [:workspace-local :selected])]
shapes (dwg/shapes-for-grouping objects selected)] (let [[group rchanges uchanges]
(when-not (empty? shapes) (dwlh/generate-add-component selected objects page-id file-id)]
(let [;; If the selected shape is a group, we can use it. If not, (when-not (empty? rchanges)
;; 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))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/select-shapes (d/ordered-set (:id group)))))))))) (dwc/select-shapes (d/ordered-set (:id group))))))))))
(defn rename-component (defn rename-component
"Rename the component with the given id, in the current file library."
[id new-name] [id new-name]
(us/assert ::us/uuid id) (us/assert ::us/uuid id)
(us/assert ::us/string new-name) (us/assert ::us/string new-name)

View file

@ -16,6 +16,7 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.main.data.workspace.groups :as dwg]
[app.util.logging :as log] [app.util.logging :as log]
[app.util.text :as ut])) [app.util.text :as ut]))
@ -122,6 +123,78 @@
(cp/clone-object shape nil objects update-new-shape update-original-shape))) (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 (defn duplicate-component
"Clone the root shape of the component and all children. Generate new "Clone the root shape of the component and all children. Generate new
ids from all of them." 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] (:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
[beicon.core :as rx] [beicon.core :as rx]
@ -8,36 +8,9 @@
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw])) [app.main.data.workspace :as dw]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries-helpers :as dwlh]))
;; ---- 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)))))
;; ---- Helpers to manage pages and objects ;; ---- Helpers to manage pages and objects
@ -53,16 +26,32 @@
:pages-index {}} :pages-index {}}
:workspace-libraries {}}) :workspace-libraries {}})
(def ^:private idmap (atom {}))
(defn reset-idmap! []
(reset! idmap {}))
(defn current-page (defn current-page
[state] [state]
(let [page-id (:current-page-id state)] (let [page-id (:current-page-id state)]
(get-in state [:workspace-data :pages-index page-id]))) (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 (defn sample-page
([state] (sample-page state {})) ([state] (sample-page state {}))
([state {:keys [id name] :as props ([state {:keys [id name] :as props
:or {id (uuid/next) :or {id (uuid/next)
name "page1"}}] name "page1"}}]
(swap! idmap assoc :page id)
(-> state (-> state
(assoc :current-page-id id) (assoc :current-page-id id)
(update :workspace-data (update :workspace-data
@ -72,13 +61,14 @@
:name name}])))) :name name}]))))
(defn sample-shape (defn sample-shape
([state type] (sample-shape state type {})) ([state label type] (sample-shape state type {}))
([state type props] ([state label type props]
(let [page (current-page state) (let [page (current-page state)
frame (cph/get-top-frame (:objects page)) frame (cph/get-top-frame (:objects page))
shape (-> (cp/make-minimal-shape type) shape (-> (cp/make-minimal-shape type)
(gsh/setup {:x 0 :y 0 :width 1 :height 1}) (gsh/setup {:x 0 :y 0 :width 1 :height 1})
(merge props))] (merge props))]
(swap! idmap assoc label (:id shape))
(update state :workspace-data (update state :workspace-data
cp/process-changes cp/process-changes
[{:type :add-obj [{:type :add-obj
@ -87,3 +77,30 @@
:frame-id (:id frame) :frame-id (:id frame)
:obj shape}])))) :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] (:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
[beicon.core :as rx] [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.data :as d]
[app.common.uuid :as uuid]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.libraries-helpers :as dwlh])) [app.main.data.workspace.libraries-helpers :as dwlh]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
(t/deftest test-create-page (t/deftest test-create-page
(t/testing "create page" (t/testing "create page"
(let [state (-> th/initial-state (let [state (-> thp/initial-state
(th/sample-page)) (thp/sample-page))
page (th/current-page state)] page (thp/current-page state)]
(t/is (= (:name page) "page1"))))) (t/is (= (:name page) "page1")))))
(t/deftest test-create-shape (t/deftest test-create-shape
(t/testing "create shape" (t/testing "create shape"
(let [id (uuid/next) (let [state (-> thp/initial-state
state (-> th/initial-state (thp/sample-page)
(th/sample-page) (thp/sample-shape :shape1 :rect
(th/sample-shape :rect {:id id {:name "Rect 1"}))
:name "Rect 1"})) shape (thp/get-shape state :shape1)]
page (th/current-page state)
shape (cph/get-shape page id)]
(t/is (= (:name shape) "Rect 1"))))) (t/is (= (:name shape) "Rect 1")))))
(t/deftest synctest (t/deftest synctest
(t/testing "synctest" (t/testing "synctest"
(let [state {:workspace-local {:color-for-rename "something"}} (let [state {:workspace-local {:color-for-rename "something"}}
new-state (->> state new-state (->> state
(th/do-update (the/do-update
dwl/clear-color-for-rename))] dwl/clear-color-for-rename))]
(t/is (= (get-in new-state [:workspace-local :color-for-rename]) (t/is (= (get-in new-state [:workspace-local :color-for-rename])
nil))))) nil)))))
@ -43,7 +46,7 @@
(let [state {} (let [state {}
color {:color "#ffffff"}] color {:color "#ffffff"}]
(->> state (->> state
(th/do-watch-update (the/do-watch-update
(dwl/add-recent-color color)) (dwl/add-recent-color color))
(rx/map (rx/map
(fn [new-state] (fn [new-state]
@ -56,43 +59,148 @@
[color])))) [color]))))
(rx/subs done)))))) (rx/subs done))))))
(t/deftest test-add-component (t/deftest test-add-component-from-single-shape
(t/testing "Add a component" (t/testing "Add a component from a single shape"
(t/async done (t/async done
(let [id1 (uuid/next) (let [state (-> thp/initial-state
state (-> th/initial-state (thp/sample-page)
(th/sample-page) (thp/sample-shape :shape1 :rect
(th/sample-shape :rect {:name "Rect 1"}))]
{:id id1
:name "Rect 1"}))]
(->> state (->> state
(th/do-update (dw/select-shape id1)) (the/do-update (dw/select-shape (thp/id :shape1)))
(th/do-watch-update dwl/add-component) (the/do-watch-update dwl/add-component)
(rx/map (rx/map
(fn [new-state] (fn [new-state]
(let [page (th/current-page new-state) (let [shape1 (thp/get-shape new-state :shape1)
shape (cph/get-shape page id1)
group (cph/get-shape page (:parent-id shape))
component (cph/get-component [[group shape1] [c-group c-shape1] component]
(:component-id group) (thl/resolve-instance-and-master
(:current-file-id new-state) new-state
(dwlh/get-local-file new-state) (:parent-id shape1))
nil)
c-shape (cph/get-shape file (dwlh/get-local-file new-state)]
component
(:shape-ref shape))
c-group (cph/get-shape (t/is (= (:name shape1) "Rect 1"))
component
(:shape-ref group))]
(t/is (= (:name shape) "Rect 1"))
(t/is (= (:name group) "Component-1")) (t/is (= (:name group) "Component-1"))
(t/is (= (:name component) "Component-1")) (t/is (= (:name component) "Component-1"))
(t/is (= (:name c-shape) "Rect 1")) (t/is (= (:name c-shape1) "Rect 1"))
(t/is (= (:name c-group) "Component-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)))))) (rx/subs done))))))