diff --git a/.circleci/config.yml b/.circleci/config.yml index 18d7cf5d7..52ec3b79e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,6 +102,16 @@ jobs: yarn install yarn test + - run: + name: "frontend integration tests" + working_directory: "./frontend" + command: | + yarn install + yarn run compile + clojure -M:dev:shadow-cljs compile main + yarn playwright install --with-deps chromium + yarn e2e:test + - run: name: "backend tests" working_directory: "./backend" diff --git a/.gimlet/penpot-prod.yaml b/.gimlet/penpot-prod.yaml new file mode 100644 index 000000000..173fbf7c6 --- /dev/null +++ b/.gimlet/penpot-prod.yaml @@ -0,0 +1,142 @@ +app: penpot +env: prod +namespace: apps +deploy: + branch: token-studio-develop + event: push +manifests: | + apiVersion: v1 + kind: Namespace + metadata: + name: penpot + labels: + toolkit.fluxcd.io/tenant: penpot-team + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + name: codechem + namespace: penpot + spec: + interval: 5m + url: https://charts.codechem.com + --- + apiVersion: bitnami.com/v1alpha1 + kind: SealedSecret + metadata: + creationTimestamp: null + name: db-penpot-secrets + namespace: penpot + spec: + encryptedData: + password: AgBzAKLzhBGDrga3ojwgBnbaDmzxQkfoIcu90ji4iutq7t2OQCuJ/8NFD1KUw8hmQ6FlwQY3reaGqRnONdzdM2VyHQmXkaoXEzCAiARh9CWiwzwW2PG6KbSmHzo/YAt17Vkux0euc0z4JAceWqbXdm8Tl8FgUktFmJNY0OGIJ8CfLFNX8p6YujSoYpIRwjG0juiGhbPGeSkJguAAR7uwLwtjCNfFRuSqEDYeaRYHvhxGgh6pyJ70+qVzUQClFJEkVzNJu9CyGx48WSPDDpPbp+h84AWIIY25Cphk48DK/oNVikQitgMVOVBU8swcpz7MSVmKxs407vKRAWN4MGV2HkNrFwFjpQsksNAQ0KTfQrVigz1Hf985w4hji1gjifK7GbSgD9Kzz8pMni3gPMj0mr4y6Nhes+hc8AGTD3N+bhpJsAZKMzSZesdamWDiwyLi+ZPuPu+1/LBVLL68DAp6odKaposQfxeTKAkxqt/6s5jvKWPl3kQ9ud2cg/8Mw3B5pqzKK4dUwUdI1pNV4GyLTj9b+M1aDYaqGmYLzZVcYxeBVh27EFC2aon8/3zkXy6Hm/BZK/aZkrmO5sJTQRYRjnlG6rRtHCWcnXI6KKqKjU5GDFk2otqrlxPMQyXyjbwycP3rTHmhAaHWkR+fOETcq+kNbVUcaR3XTCw7T1qFZ4dtaBN02RHbRE3qxs/SjGMPnzfKQs1626gHAayZqxprpfz6mT0u0Hkn2NGg6RlJr36CxfE= + username: AgBw5ALuBj1TpQc5dmyLW927WQO9AXgdyqYeXHwzXbLKIdyAkyihVIkTSD/MS/InTbsiFIYPvZptpAjpWc9p2IN8nvLbEjc8JXS7DA3NDr/SN7J70oDOKS/vT4Vlz4yX/6fmU8pGvjMh22ELBbruxWS+a6Nty/XcZPqJ8gMuj/vAnticq+i4Rmuy1aghEfsYzPVSigS5QfnnFsMBA5lZS7rgiv4voudi5aAh8luIsDx9eCk2WxcN+9f816MYXBxcZL853h4lIQziOfs8LK0jCZm62yOeckmuMt0EznGEwAS1Magrw9PnZdSDOHvTrugRT/sx8JzkpEorJQXTA/6hXT9tqTbZuLnHMcdVGAcU9+1QcJPtlhYH05irbDqMs5IgxqCW7ch3gtiIS1hTRGpaG+LoNGREcZZtiWxkgcVhJG8E+5ailyt0B/NO+RgjYjjK+tH/hcGd2hABvkmS1f9FUHIRdE0uiwvwM/hWU9qTJcSHdN3mJ96/7lQvfnDoDDP8zS09Co0E0zLmLFSAEvOIz7HMvE0Bw2UPzcy4N8J2y+u4m0327FUUN96Y3e2L+o1SrVw/CJO1/haN34j1SMUFh/4q63VvNLDfUD69QbpjMtjNrvhqNWyyET1QNWl4SFsfbMdC7/rXM9Lpg4GEZ6R5G/QcTb27Zo5UuOeFP060XiWJ1/bD8tiZKU1K1QTwJ0Uur3MDcrYRvGw= + template: + metadata: + creationTimestamp: null + name: db-penpot-secrets + namespace: penpot + type: Opaque + --- + apiVersion: bitnami.com/v1alpha1 + kind: SealedSecret + metadata: + creationTimestamp: null + name: db-penpot-superuser-secret + namespace: penpot + spec: + encryptedData: + password: AgBwTdp950SD3x9c1CjlAz4MGEN3tTDQH0iKLW1e4itCEB+W7c6hf+t2nc4VYLAhxCbMfs+sS2onSuoIuzr7/wTLtia3gSaRAgPFu2t91m6s51ewMMrPxoAmIdpHiojCnBXdhuc6XjinOs40MOoS6/qY1WjEXaPyvKBeMdFkKAdDTvMW6WA9xel8Jyf3U6Tz8/Onj1VAAnhgehFvPMZ1uDCEtUfKDPAe+za4S1SRAL81iNwJCVQJrQdetDpcIMnKkMbUvy8RFDmPPKJ6lxZHFk8ztJgCXlj55ViWlEmUC3QHtktnB0QYd+B2rFf6j/66ozgzyiqHd4nXCuRiCxnFSgFMrWYbaDheJEN4rgDNZBITIBiqnlH5HntieQHj9YohsVkr7r0FObtKpePV1t+Sb0RptJ9+LWexkSs6Rvq3HNj7JdOLN/QVsIZbiU4ctRMjxiVsyl7PDZe84tx3Kl6BiUOrClN8QR6huLLnIdVXetMbrPDDQOCI4FiH5UghLRlPdNkvpoeYLfL79Mxy5yOG+xkydM5HR7//NMGDqP7hf+vZFe8/EKuaSExUX6S0AT+hQVkmWcwy7OKq2Ra37XezjmWf6KGiHAL8Idn47E+PQ5axAlkZ1MgkjB+pc/2Lpyo3bfINa7avf03nOKwJl01cChB9O4bDkUfDh7N+26YkqlwMm6aU4dm80fydsPRBikTKTWafpLsQqtimv1ANTYHvbDDEsufK95O/cq8ER/fTAmmrcg== + username: AgAIwqIbS5Ze9e4sefyg77opd801epCHPewxEb+VuuJrxIl4+gFroopQNf/lhRQRFX1unI0PaR/iV5szaIaDmIYz3JQ3OxCyF1zeDYr6YhYNQMtkLgRJRrBr0j7TQPAKwLmgtZok7hDIvTj5bQ2dydibQ1Zg8N1valb03X6Vs/ivvvO3KvQEDckdGgD7UauL8onapU6KAFU5Hu9aEkvMTk4CGNpuLxGhYdA1+HLpLdQYBQPaJyEblGoko4wyv+pF/3m1tRBSQ1L8HsUXfAEn5dAd+0qkS5IvwOM8zXnZcT0ohXmY2mXjvPyv2phOKElQYURPwq9PPI+Sc5Ff7QHcVLjwQ+DYtSMDlRSt5BektWC+peBJfxZQ/X6w2AmICtdkOT03rrMxn0sWKIgQkvmJ0jkERAYlvifcoRmbof0wbFh+ANa0NpGLvxwiG+DsQ6eAsTB0Nu3wPCCBFZOUuTxS+yb4BjE3KPNGgVI6XtArxPnO3z3xglfI/nKDTY1rC2e0ZE72BDnhsLAwQTTgb/R/X2mvS7a0YXJ0gOpAgwP92K9zy1GA9ov2uTVZ2wbb39E69OxMKcbetDWirQrSYMqLYzJ1+W2cBbNdCcYQ2xnSM9cdEd2sPcFJ3NDVQeQRhxSTI6UfKWphUeksqwdW+VN7aODlUzMSxBCwGnxuaS6OVzOdLzQnORdyhyD8zclh5e0AXJpvqs+Z4CuvZA== + template: + metadata: + creationTimestamp: null + name: db-penpot-superuser-secret + namespace: penpot + type: Opaque + --- + apiVersion: postgresql.cnpg.io/v1 + kind: Cluster + metadata: + name: penpot-db + namespace: penpot + spec: + instances: 1 + superuserSecret: + name: db-penpot-superuser-secret + bootstrap: + initdb: + database: penpot + owner: penpot + secret: + name: db-penpot-secrets + monitoring: + enablePodMonitor: true + storage: + size: 5Gi + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta2 + kind: HelmRelease + metadata: + name: penpot + namespace: penpot + spec: + releaseName: penpot + chart: + spec: + version: "1.0.10" + chart: penpot + sourceRef: + kind: HelmRepository + name: codechem + interval: 50m + install: + remediation: + retries: 3 + values: + redis: + replica: + replicaCount: 0 + global: + postgresqlEnabled: false + redisEnabled: true + imagePullSecrets: + - name: ghcr-login-secret + backend: + image: + pullPolicy: IfNotPresent + repository: ghcr.io/tokens-studio/tokens-studio-for-penpot + tag: 'backend-{{ .SHA }}' + frontend: + image: + pullPolicy: IfNotPresent + repository: ghcr.io/tokens-studio/tokens-studio-for-penpot + tag: 'frontend-{{ .SHA }}' + ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + networking.gke.io/v1beta1.FrontendConfig: default-frontend-config + hosts: + - host: penpot.tokens.studio + tls: + - secretName: tls-penpot + hosts: + - penpot.tokens.studio + # https://github.com/codechem/helm/issues/15 + ingress: + tls: + - secretName: tls-penpot + hosts: + - penpot.tokens.studio + config: + publicURI: https://penpot.tokens.studio + redis: + host: penpot-redis-master.penpot.svc.cluster.local + postgresql: + host: penpot-db-rw + database: penpot + existingSecret: db-penpot-secrets + secretKeys: + usernameKey: username + passwordKey: password \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..b9b45560c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,73 @@ +name: Publish docker image + +on: + push: + branches: [ token-studio-develop ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - dockerfile: Dockerfile.backend + type: backend + - dockerfile: Dockerfile.frontend + type: frontend + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + tags: | + type=sha,format=long,prefix=${{matrix.type}}- + images: | + ghcr.io/tokens-studio/tokens-studio-for-penpot + - name: prebuild + run: | + echo "Building ${{ matrix.type }} image" + ./manage.sh build-${{matrix.type}}-bundle + mv ./bundles/${{matrix.type}} ./docker/images/bundle-${{matrix.type}}/ + + - name: Publish Docker images + uses: docker/build-push-action@v5 + with: + context: ./docker/images + file: ./docker/images/${{ matrix.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + + deploy: + runs-on: ubuntu-latest + needs: + - "build" + steps: + - uses: actions/checkout@v4 + - name: 🍍 Deploy with Gimlet + uses: gimlet-io/gimlet-artifact-shipper-action@v0.8.3 + env: + GIMLET_SERVER: ${{ secrets.GIMLET_SERVER }} + GIMLET_TOKEN: ${{ secrets.GIMLET_TOKEN }} + with: + ENV: "prod" + APP: "penpot" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0e271d125..9b3cdd4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,7 @@ clj-profiler/ node_modules frontend/.storybook/preview-body.html +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 83afd2b92..c5f28cb9b 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -744,6 +744,7 @@ (map lookupf) (map mk-change)) updated-shapes)))) + (apply-changes-local))))) (defn update-component diff --git a/common/src/app/common/files/libraries_common_helpers.cljc b/common/src/app/common/files/libraries_common_helpers.cljc deleted file mode 100644 index e7df2d8f3..000000000 --- a/common/src/app/common/files/libraries_common_helpers.cljc +++ /dev/null @@ -1,103 +0,0 @@ -;; 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 INC - -(ns app.common.files.libraries-common-helpers - (:require - [app.common.data :as d] - [app.common.files.changes-builder :as pcb] - [app.common.files.helpers :as cfh] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.uuid :as uuid])) - -(defn generate-add-component-changes - [changes root objects file-id page-id components-v2] - (let [name (:name root) - [path name] (cfh/parse-path-name name) - - [root-shape new-shapes updated-shapes] - (if-not components-v2 - (ctn/make-component-shape root objects file-id components-v2) - (ctn/convert-shape-in-component root objects file-id)) - - changes (-> changes - (pcb/add-component (:id root-shape) - path - name - new-shapes - updated-shapes - (:id root) - page-id))] - [root-shape changes])) - -(defn generate-add-component - "If there is exactly one id, and it's a frame (or a group in v1), and not already a component, - use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a - component with it, and link all shapes to their corresponding one in the component." - [it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board] - - (let [changes (pcb/empty-changes it page-id) - shapes-count (count shapes) - first-shape (first shapes) - - from-singe-frame? - (and (= 1 shapes-count) - (cfh/frame-shape? first-shape)) - - [root changes old-root-ids] - (if (and (= shapes-count 1) - (or (and (cfh/group-shape? first-shape) - (not components-v2)) - (cfh/frame-shape? first-shape)) - (not (ctk/instance-head? first-shape))) - [first-shape - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) - (:shapes first-shape)] - - (let [root-name (if (= 1 shapes-count) - (:name first-shape) - "Component 1") - - shape-ids (into (d/ordered-set) (map :id) shapes) - - [root changes] - (if-not components-v2 - (prepare-create-group it ; These functions needs to be passed as argument - objects ; to avoid a circular dependence - page-id - shapes - root-name - (not (ctk/instance-head? first-shape))) - (prepare-create-board changes - (uuid/next) - (:parent-id first-shape) - objects - shape-ids - nil - root-name - true))] - - [root changes shape-ids])) - - changes - (cond-> changes - (not from-singe-frame?) - (pcb/update-shapes - (:shapes root) - (fn [shape] - (assoc shape :constraints-h :scale :constraints-v :scale)))) - - objects' (assoc objects (:id root) root) - - [root-shape changes] (generate-add-component-changes changes root objects' file-id page-id components-v2) - - changes (pcb/update-shapes changes - old-root-ids - #(dissoc % :component-root) - [:component-root])] - - [root (:id root-shape) changes])) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 458bcb2ec..ccc2f5d34 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -22,8 +22,10 @@ [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.types.shape-tree :as ctst] + [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] [app.common.types.typography :as cty] [app.common.uuid :as uuid] @@ -148,8 +150,6 @@ [new-component-shape new-component-shapes nil nil])))) - - (defn generate-duplicate-component "Create a new component copied from the one with the given id." [changes library component-id components-v2] @@ -1923,3 +1923,295 @@ (cond-> changes (some? swap-slot) (generate-sync-head file-full libraries container id components-v2 true)))) + +(defn generate-duplicate-flows + [changes shapes page ids-map] + (let [flows (-> page :options :flows) + unames (volatile! (into #{} (map :name flows))) + frames-with-flow (->> shapes + (filter #(= (:type %) :frame)) + (filter #(some? (ctp/get-frame-flow flows (:id %)))))] + (if-not (empty? frames-with-flow) + (let [update-flows (fn [flows] + (reduce + (fn [flows frame] + (let [name (cfh/generate-unique-name @unames "Flow 1") + _ (vswap! unames conj name) + new-flow {:id (uuid/next) + :name name + :starting-frame (get ids-map (:id frame))}] + (ctp/add-flow flows new-flow))) + flows + frames-with-flow))] + (pcb/update-page-option changes :flows update-flows)) + changes))) + +(defn generate-duplicate-guides + [changes shapes page ids-map delta] + (let [guides (get-in page [:options :guides]) + frames (->> shapes (filter cfh/frame-shape?)) + + new-guides + (reduce + (fn [g frame] + (let [new-id (ids-map (:id frame)) + new-frame (-> frame (gsh/move delta)) + + new-guides + (->> guides + (vals) + (filter #(= (:frame-id %) (:id frame))) + (map #(-> % + (assoc :id (uuid/next)) + (assoc :frame-id new-id) + (assoc :position (if (= (:axis %) :x) + (+ (:position %) (- (:x new-frame) (:x frame))) + (+ (:position %) (- (:y new-frame) (:y frame))))))))] + (cond-> g + (not-empty new-guides) + (conj (into {} (map (juxt :id identity) new-guides)))))) + guides + frames)] + (-> (pcb/with-page changes page) + (pcb/set-page-option :guides new-guides)))) + +(defn generate-duplicate-component-change + [changes objects page component-root parent-id frame-id delta libraries library-data] + (let [component-id (:component-id component-root) + file-id (:component-file component-root) + main-component (ctf/get-component libraries file-id component-id) + moved-component (gsh/move component-root delta) + pos (gpt/point (:x moved-component) (:y moved-component)) + origin-frame (get-in page [:objects frame-id]) + delta (cond-> delta + (some? origin-frame) + (gpt/subtract (-> origin-frame :selrect gpt/point))) + + instantiate-component + #(generate-instantiate-component changes + objects + file-id + (:component-id component-root) + pos + page + libraries + (:id component-root) + parent-id + frame-id + {}) + + restore-component + #(let [restore (prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] + [(:shape restore) (:changes restore)]) + + [_shape changes] + (if (nil? main-component) + (restore-component) + (instantiate-component))] + changes)) + +(defn generate-duplicate-shape-change + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id] + (generate-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true)) + + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] + (cond + (nil? obj) + changes + + (ctf/is-main-of-known-component? obj libraries) + (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) + + :else + (let [frame? (cfh/frame-shape? obj) + group? (cfh/group-shape? obj) + bool? (cfh/bool-shape? obj) + new-id (ids-map (:id obj)) + parent-id (or parent-id frame-id) + parent (get objects parent-id) + name (:name obj) + + is-component-root? (or (:saved-component-root obj) + ;; Backward compatibility + (:saved-component-root? obj) + (ctk/instance-root? obj)) + duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) + is-component-main? (ctk/main-instance? obj) + subinstance-head? (ctk/subinstance-head? obj) + instance-root? (ctk/instance-root? obj) + + into-component? (and duplicating-component? + (ctn/in-any-component? objects parent)) + + level-delta (if (some? level-delta) + level-delta + (ctn/get-nesting-level-delta objects obj parent)) + new-shape-ref (ctf/advance-shape-ref nil page libraries obj level-delta {:include-deleted? true}) + + regenerate-component + (fn [changes shape] + (let [components-v2 (dm/get-in library-data [:options :components-v2]) + [_ changes] (generate-add-component-changes changes shape objects file-id (:id page) components-v2)] + changes)) + + new-obj + (-> obj + (assoc :id new-id + :name name + :parent-id parent-id + :frame-id frame-id) + + (cond-> (and (not instance-root?) + subinstance-head? + remove-swap-slot?) + (ctk/remove-swap-slot)) + + (dissoc :shapes + :use-for-thumbnail) + + (cond-> (not is-component-root?) + (dissoc :main-instance)) + + (cond-> into-component? + (dissoc :component-root)) + + (cond-> (and (ctk/instance-head? obj) + (not into-component?)) + (assoc :component-root true)) + + (cond-> (or frame? group? bool?) + (assoc :shapes [])) + + (cond-> (and (some? new-shape-ref) + (not= new-shape-ref (:shape-ref obj))) + (assoc :shape-ref new-shape-ref)) + + (gsh/move delta) + (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) + + (cond-> (ctl/grid-layout? obj) + (ctl/remap-grid-cells ids-map))) + + new-obj (cond-> new-obj + (not duplicating-component?) + (ctk/detach-shape)) + + ;; We want the first added object to touch it's parent, but not subsequent children + changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) + (pcb/amend-last-change #(assoc % :old-id (:id obj))) + (cond-> (ctl/grid-layout? objects (:parent-id obj)) + (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) + (pcb/reorder-grid-children [(:parent-id obj)])))) + + changes (cond-> changes + (and is-component-root? is-component-main?) + (regenerate-component new-obj)) + + ;; This is needed for the recursive call to find the new object as parent + page' (ctst/add-shape (:id new-obj) + new-obj + {:objects objects} + (:frame-id new-obj) + (:parent-id new-obj) + nil + true)] + + (reduce (fn [changes child] + (generate-duplicate-shape-change changes + (:objects page') + page + unames + update-unames! + ids-map + child + delta + level-delta + libraries + library-data + file-id + (if frame? new-id frame-id) + new-id + duplicating-component? + true + (and remove-swap-slot? + ;; only remove swap slot of children when the current shape + ;; is not a subinstance head nor a instance root + (not subinstance-head?) + (not instance-root?)))) + changes + (map (d/getf objects) (:shapes obj))))))) + +(defn generate-duplicate-changes + "Prepare objects to duplicate: generate new id, give them unique names, + move to the desired position, and recalculate parents and frames as needed." + [changes all-objects page ids delta libraries library-data file-id] + (let [shapes (map (d/getf all-objects) ids) + unames (volatile! (cfh/get-used-names (:objects page))) + update-unames! (fn [new-name] (vswap! unames conj new-name)) + all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) + + ;; We need ids-map for remapping the grid layout. But when duplicating the guides + ;; we calculate a new one because the components will have created new shapes. + ids-map (into {} (map #(vector % (uuid/next))) all-ids) + + changes (-> changes + (pcb/with-page page) + (pcb/with-objects all-objects)) + changes + (->> shapes + (reduce #(generate-duplicate-shape-change %1 + all-objects + page + unames + update-unames! + ids-map + %2 + delta + nil + libraries + library-data + file-id) + changes)) + + ;; We need to check the changes to get the ids-map + ids-map + (into {} + (comp + (filter #(= :add-obj (:type %))) + (map #(vector (:old-id %) (-> % :obj :id)))) + (:redo-changes changes))] + + (-> changes + (generate-duplicate-flows shapes page ids-map) + (generate-duplicate-guides shapes page ids-map delta)))) + +(defn generate-duplicate-changes-update-indices + "Updates the changes to correctly set the indexes of the duplicated objects, + depending on the index of the original object respect their parent." + [changes objects ids] + (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) + index-map (reduce (fn [index-map id] + (let [parent-id (get-in objects [id :parent-id]) + parent-index (cfh/get-position-on-parent objects id)] + (update index-map parent-id (fnil conj []) [id parent-index]))) + {} + ids) + + inc-indices + (fn [[offset result] [id index]] + [(inc offset) (conj result [id (+ index offset)])]) + + fix-indices + (fn [_ entry] + (->> entry + (sort-by second) + (reduce inc-indices [1 []]) + (second) + (into {}))) + + objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] + + (pcb/amend-changes + changes + (fn [change] + (assoc change :index (get objects-indices (:old-id change))))))) diff --git a/common/test/common_tests/helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc similarity index 90% rename from common/test/common_tests/helpers/components.cljc rename to common/src/app/common/test_helpers/components.cljc index 56ebf488d..b1c9c0513 100644 --- a/common/test/common_tests/helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -4,22 +4,22 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns common-tests.helpers.components +(ns app.common.test-helpers.components (:require [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.logic.libraries :as cll] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] [app.common.types.pages-list :as ctpl] - [app.common.types.shape-tree :as ctst] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi] - [common-tests.helpers.shapes :as ths])) + [app.common.types.shape-tree :as ctst])) (defn make-component [file label root-label & {:keys [] :as params}] @@ -33,8 +33,9 @@ (let [[_new-root _new-shapes updated-shapes] (ctn/convert-shape-in-component root (:objects page) (:id file)) - updated-root (first updated-shapes)] ; Can't use new-root because it has a new id + updated-root (first updated-shapes) ; Can't use new-root because it has a new id + [path name] (cfh/parse-path-name (:name updated-root))] (thi/set-id! label (:component-id updated-root)) (ctf/update-file-data @@ -49,14 +50,15 @@ updated-shapes) (ctkl/add-component $ (assoc params :id (:component-id updated-root) - :name (:name updated-root) + :name name + :path path :main-instance-id (:id updated-root) :main-instance-page (:id page) :shapes updated-shapes)))))))) (defn get-component - [file label] - (ctkl/get-component (:data file) (thi/id label))) + [file label & {:keys [include-deleted?] :or {include-deleted? false}}] + (ctkl/get-component (:data file) (thi/id label) include-deleted?)) (defn get-component-by-id [file id] @@ -129,6 +131,7 @@ (when children-labels (dotimes [idx (count children-labels)] (set-child-label file' copy-root-label idx (nth children-labels idx)))) + file')) (defn component-swap diff --git a/common/test/common_tests/helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc similarity index 95% rename from common/test/common_tests/helpers/compositions.cljc rename to common/src/app/common/test_helpers/compositions.cljc index cf1a02d9a..72ab0073f 100644 --- a/common/test/common_tests/helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -4,11 +4,11 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns common-tests.helpers.compositions +(ns app.common.test-helpers.compositions (:require [app.common.data :as d] - [common-tests.helpers.components :as thc] - [common-tests.helpers.shapes :as ths])) + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.shapes :as ths])) (defn add-rect [file rect-label & {:keys [] :as params}] @@ -140,8 +140,8 @@ component2-params))) (defn add-nested-component-with-copy - [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-label - & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-params]}] + [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label + & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}] ;; Generated shape tree: ;; {:main1-root-label} [:name: Frame1] # [Component :component1-label] ;; :main1-child-label [:name: Rect1] @@ -166,4 +166,4 @@ :component2-params component2-params :main2-root-params main2-root-params :nested-head-params nested-head-params) - (thc/instantiate-component component2-label copy2-label copy2-params))) \ No newline at end of file + (thc/instantiate-component component2-label copy2-root-label copy2-root-params))) \ No newline at end of file diff --git a/common/test/common_tests/helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc similarity index 53% rename from common/test/common_tests/helpers/files.cljc rename to common/src/app/common/test_helpers/files.cljc index 2d893104f..59b166555 100644 --- a/common/test/common_tests/helpers/files.cljc +++ b/common/src/app/common/test_helpers/files.cljc @@ -4,18 +4,19 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns common-tests.helpers.files +(ns app.common.test-helpers.files (:require [app.common.data :as d] [app.common.features :as ffeat] [app.common.files.changes :as cfc] [app.common.files.validate :as cfv] [app.common.pprint :refer [pprint]] + [app.common.test-helpers.ids-map :as thi] + [app.common.types.component :as ctk] [app.common.types.file :as ctf] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.uuid :as uuid] - [common-tests.helpers.ids-map :as thi] [cuerdas.core :as str])) ;; ----- Files @@ -87,7 +88,7 @@ ;; ----- Debug -(defn dump-file-type +(defn dump-tree "Dump a file using dump-tree function in common.types.file." [file & {:keys [page-label libraries] :as params}] (let [params (-> params @@ -115,44 +116,74 @@ (println "}")) (defn- stringify-keys [m keys] - (apply str (interpose ", " (map #(str % ": " (get m %)) keys)))) + (let [kv (-> (select-keys m keys) + (assoc :swap-slot (when ((set keys) :swap-slot) + (ctk/get-swap-slot m))) + (assoc :swap-slot-label (when ((set keys) :swap-slot-label) + (when-let [slot (ctk/get-swap-slot m)] + (thi/label slot)))) + (d/without-nils)) + + pretty-uuid (fn [id] + (let [id (str id)] + (str "#" (subs id (- (count id) 6))))) + + format-kv (fn [[k v]] + (cond + (uuid? v) + (str k " " (pretty-uuid v)) + + :else + (str k " " v)))] + + (when (seq kv) + (str " [" (apply str (interpose ", " (map format-kv kv))) "]")))) (defn- dump-page-shape - [shape keys padding] + [shape keys padding show-refs?] (println (str/pad (str padding - (when (:main-instance shape) "{") - (or (thi/label (:id shape)) "") - (when (:main-instance shape) "}") - (when keys - (str " [" (stringify-keys shape keys) "]"))) - {:length 40 :type :right}) + (when (and (:main-instance shape) show-refs?) "{") + (thi/label (:id shape)) + (when (and (:main-instance shape) show-refs?) "}") + (when (seq keys) + (stringify-keys shape keys))) + {:length 50 :type :right}) (if (nil? (:shape-ref shape)) - (if (:component-root shape) - (str "# [Component " (or (thi/label (:component-id shape)) "") "]") + (if (and (:component-root shape) show-refs?) + (str "# [Component " (thi/label (:component-id shape)) "]") "") - (str/format "%s--> %s%s" - (cond (:component-root shape) "#" - (:component-id shape) "@" - :else "-") - (if (:component-root shape) - (str "[Component " (or (thi/label (:component-id shape)) "") "] ") - "") - (or (thi/label (:shape-ref shape)) ""))))) + (if show-refs? + (str/format "%s--> %s%s" + (cond (:component-root shape) "#" + (:component-id shape) "@" + :else "-") + (if (:component-root shape) + (str "[Component " (thi/label (:component-id shape)) "] ") + "") + (thi/label (:shape-ref shape))) + "")))) (defn dump-page - "Dump the layer tree of the page. Print the label of each shape, and the specified keys." - ([page keys] - (dump-page page uuid/zero "" keys)) - ([page root-id padding keys] - (let [lookupf (d/getf (:objects page)) - root-shape (lookupf root-id) - shapes (map lookupf (:shapes root-shape))] - (doseq [shape shapes] - (dump-page-shape shape keys padding) - (dump-page page (:id shape) (str padding " ") keys))))) + "Dump the layer tree of the page, showing labels of the shapes. + - keys: a list of attributes of the shapes you want to show. In addition, you + can add :swap-slot to show the slot id (if any) or :swap-slot-label + to show the corresponding label. + - show-refs?: if true, the component references will be shown." + [page & {:keys [keys root-id padding show-refs?] + :or {keys [:name :swap-slot-label] root-id uuid/zero padding "" show-refs? true}}] + (let [lookupf (d/getf (:objects page)) + root-shape (lookupf root-id) + shapes (map lookupf (:shapes root-shape))] + (doseq [shape shapes] + (dump-page-shape shape keys padding show-refs?) + (dump-page page + :keys keys + :root-id (:id shape) + :padding (str padding " ") + :show-refs? show-refs?)))) (defn dump-file "Dump the current page of the file, using dump-page above. - Example: (thf/dump-file file [:id :touched])" - ([file] (dump-file file [])) - ([file keys] (dump-page (current-page file) keys))) + Example: (thf/dump-file file :keys [:name :swap-slot-label] :show-refs? false)" + [file & {:keys [] :as params}] + (dump-page (current-page file) params)) diff --git a/common/test/common_tests/helpers/ids_map.cljc b/common/src/app/common/test_helpers/ids_map.cljc similarity index 77% rename from common/test/common_tests/helpers/ids_map.cljc rename to common/src/app/common/test_helpers/ids_map.cljc index cec7242cf..603229d9e 100644 --- a/common/test/common_tests/helpers/ids_map.cljc +++ b/common/src/app/common/test_helpers/ids_map.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns common-tests.helpers.ids-map +(ns app.common.test-helpers.ids-map (:require [app.common.uuid :as uuid])) @@ -36,7 +36,8 @@ (f)) (defn label [id] - (->> @idmap - (filter #(= id (val %))) - (map key) - (first))) + (or (->> @idmap + (filter #(= id (val %))) + (map key) + (first)) + (str ""))) diff --git a/common/test/common_tests/helpers/shapes.cljc b/common/src/app/common/test_helpers/shapes.cljc similarity index 95% rename from common/test/common_tests/helpers/shapes.cljc rename to common/src/app/common/test_helpers/shapes.cljc index 53521cbe5..28e8c5d2c 100644 --- a/common/test/common_tests/helpers/shapes.cljc +++ b/common/src/app/common/test_helpers/shapes.cljc @@ -4,10 +4,12 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns common-tests.helpers.shapes +(ns app.common.test-helpers.shapes (:require [app.common.colors :as clr] [app.common.files.helpers :as cfh] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] [app.common.types.color :as ctc] [app.common.types.colors-list :as ctcl] [app.common.types.file :as ctf] @@ -15,9 +17,7 @@ [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.typographies-list :as cttl] - [app.common.types.typography :as ctt] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi])) + [app.common.types.typography :as ctt])) (defn sample-shape [label & {:keys [type] :as params}] diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index a76189f71..c7a301ca4 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -61,6 +61,10 @@ (update container :objects update-objects parent-id))) +(defn parent-of? + [parent child] + (= (:id parent) (:parent-id child))) + (defn get-shape "Get a shape identified by id" [container id] diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index d9a107054..67b86ef8c 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -35,6 +35,7 @@ (def token-types #{:boolean :border-radius + :stroke-width :box-shadow :dimension :numeric @@ -66,6 +67,12 @@ (def border-radius-keys (schema-keys ::border-radius)) +(sm/def! ::stroke-width + [:map + [:stroke-width {:optional true} ::sm/uuid]]) + +(def stroke-width-keys (schema-keys ::stroke-width)) + (sm/def! ::dimensions [:map [:width {:optional true} ::sm/uuid] @@ -77,6 +84,12 @@ (def dimensions-keys (schema-keys ::dimensions)) +(sm/def! ::opacity + [:map + [:opacity ::sm/uuid]]) + +(def opacity-keys (schema-keys ::opacity)) + (sm/def! ::spacing [:map [:spacing-column {:optional true} ::sm/uuid] diff --git a/common/test/cases/chained-components-changes-propagation.penpot b/common/test/cases/chained-components-changes-propagation.penpot new file mode 100644 index 000000000..368e9a0bc Binary files /dev/null and b/common/test/cases/chained-components-changes-propagation.penpot differ diff --git a/common/test/common_tests/logic/chained_propagation_test.cljc b/common/test/common_tests/logic/chained_propagation_test.cljc new file mode 100644 index 000000000..dc9a5ac91 --- /dev/null +++ b/common/test/common_tests/logic/chained_propagation_test.cljc @@ -0,0 +1,212 @@ +;; 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 INC + +(ns common-tests.logic.chained-propagation-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.container :as ctn] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(defn- first-fill-color [file tag] + (-> (ths/get-shape file tag) + (:fills) + first + :fill-color)) + +(defn- first-child-fill-color [file tag] + (let [shape (ths/get-shape file tag)] + (-> (ths/get-shape-by-id file (first (:shapes shape))) + (:fills) + first + :fill-color))) + +;; Related .penpot file: common/test/cases/chained-components-changes-propagation.penpot +(t/deftest test-propagation-with-anidated-components + (letfn [(setup [] + (-> (thf/sample-file :file1) + (tho/add-frame :frame-comp-1) + (ths/add-sample-shape :rectangle :parent-label :frame-comp-1) + (thc/make-component :comp-1 :frame-comp-1) + + (tho/add-frame :frame-comp-2) + (thc/instantiate-component :comp-1 :copy-comp-1 :parent-label :frame-comp-2 :children-labels [:rect-comp-2]) + (thc/make-component :comp-2 :frame-comp-2) + + (tho/add-frame :frame-comp-3) + (thc/instantiate-component :comp-2 :copy-comp-2 :parent-label :frame-comp-3 :children-labels [:comp-1-comp-2]) + (thc/make-component :comp-3 :frame-comp-3))) + + (step-update-color-comp-2 [file] + (let [page (thf/current-page file) + + ;; Changes to update the color of the contained rectangle in component comp-2 + changes-update-color-comp-1 + (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + (:shapes (ths/get-shape file :copy-comp-1)) + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#FF0000"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes-update-color-comp-1)] + + (t/is (= (first-child-fill-color file' :comp-1-comp-2) "#B1B2B5")) + file')) + + (step-propagate-comp-2 [file] + (let [page (thf/current-page file) + file-id (:id file) + + ;; Changes to propagate the color changes of component comp-1 + changes-sync-comp-1 (-> (pcb/empty-changes) + (cll/generate-sync-file-changes + nil + :components + file-id + (:id (thc/get-component file :comp-2)) + file-id + {file-id file} + file-id)) + + file' (thf/apply-changes file changes-sync-comp-1)] + + (t/is (= (first-fill-color file' :rect-comp-2) "#FF0000")) + (t/is (= (first-child-fill-color file' :comp-1-comp-2) "#FF0000")) + file')) + + (step-update-color-comp-3 [file] + (let [page (thf/current-page file) + page-id (:id page) + comp-1-comp-2 (ths/get-shape file :comp-1-comp-2) + rect-comp-3 (ths/get-shape-by-id file (first (:shapes comp-1-comp-2))) + ;; Changes to update the color of the contained rectangle in component comp-3 + changes-update-color-comp-3 + (cls/generate-update-shapes (pcb/empty-changes nil page-id) + [(:id rect-comp-3)] + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#00FF00"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes-update-color-comp-3)] + + (t/is (= (first-child-fill-color file' :comp-1-comp-2) "#00FF00")) + file')) + + (step-reset [file] + (let [page (thf/current-page file) + file-id (:id file) + comp-1-comp-2 (ths/get-shape file :comp-1-comp-2) + ;; Changes to reset the changes on comp-1 inside comp-3 + changes-reset (cll/generate-reset-component (pcb/empty-changes) + file + {file-id file} + (ctn/make-container page :page) + (:id comp-1-comp-2) + true) + file' (thf/apply-changes file changes-reset)] + + (t/is (= (first-child-fill-color file' :comp-1-comp-2) "#FF0000")) + file'))] + + (-> (setup) + step-update-color-comp-2 + step-propagate-comp-2 + step-update-color-comp-3 + step-reset))) + +(t/deftest test-propagation-with-deleted-component + (letfn [(setup [] + (-> (thf/sample-file :file1) + (tho/add-frame :frame-comp-4) + (ths/add-sample-shape :rectangle :parent-label :frame-comp-4) + (thc/make-component :comp-4 :frame-comp-4) + + (tho/add-frame :frame-comp-5) + (thc/instantiate-component :comp-4 :copy-comp-4 :parent-label :frame-comp-5 :children-labels [:rect-comp-5]) + (thc/make-component :comp-5 :frame-comp-5) + + (tho/add-frame :frame-comp-6) + (thc/instantiate-component :comp-5 :copy-comp-5 :parent-label :frame-comp-6 :children-labels [:comp-4-comp-5]) + (thc/make-component :comp-6 :frame-comp-6))) + + (step-delete-comp-5 [file] + (let [page (thf/current-page file) + ;; Changes to delete comp-5 + [_ changes-delete] (cls/generate-delete-shapes (pcb/empty-changes nil (:id page)) + file + page + (:objects page) + #{(-> (ths/get-shape file :frame-comp-5) + :id)} + {:components-v2 true}) + + file' (thf/apply-changes file changes-delete)] + (t/is (= (first-child-fill-color file' :comp-4-comp-5) "#B1B2B5")) + file')) + + (step-update-color-comp-4 [file] + (let [page (thf/current-page file) + ;; Changes to update the color of the contained rectangle in component comp-4 + changes-update-color-comp-4 + (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + [(-> (ths/get-shape file :rectangle) + :id)] + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#FF0000"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes-update-color-comp-4)] + (t/is (= (first-fill-color file' :rectangle) "#FF0000")) + file')) + + (step-propagate-comp-4 [file] + (let [file-id (:id file) + ;; Changes to propagate the color changes of component comp-4 + changes-sync-comp-4 (-> (pcb/empty-changes) + (cll/generate-sync-file-changes + nil + :components + file-id + (:id (thc/get-component file :comp-4)) + file-id + {file-id file} + file-id)) + + file' (thf/apply-changes file changes-sync-comp-4)] + file')) + + (step-propagate-comp-5 [file] + (let [file-id (:id file) + ;; Changes to propagate the color changes of component comp-5 + changes-sync-comp-5 (-> (pcb/empty-changes) + (cll/generate-sync-file-changes + nil + :components + file-id + (:id (thc/get-component file :comp-5)) + file-id + {file-id file} + file-id)) + file' (thf/apply-changes file changes-sync-comp-5)] + (t/is (= (first-child-fill-color file' :comp-4-comp-5) "#FF0000")) + file'))] + + (-> (setup) + step-delete-comp-5 + step-update-color-comp-4 + step-propagate-comp-4 + step-propagate-comp-5))) diff --git a/common/test/common_tests/logic/comp_creation_test.cljc b/common/test/common_tests/logic/comp_creation_test.cljc new file mode 100644 index 000000000..c59c14bc1 --- /dev/null +++ b/common/test/common_tests/logic/comp_creation_test.cljc @@ -0,0 +1,610 @@ +;; 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 INC + +(ns common-tests.logic.comp-creation-test + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.files.shapes-helpers :as cfsh] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.shape-tree :as ctst] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-add-component-from-single-frame + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame)) + + page (thf/current-page file) + frame1 (ths/get-shape file :frame1) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [frame1] + (:objects page) + (:id page) + (:id file) + true + nil + nil) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + frame1' (ths/get-shape file' :frame1)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? frame1')) + (t/is (= (:id root) (:id frame1'))) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-single-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :shape1 :type :rect)) + + page (thf/current-page file) + shape1 (ths/get-shape file :shape1) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [shape1] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + shape1' (ths/get-shape file' :shape1)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? shape1')) + (t/is (ctst/parent-of? root shape1')) + (t/is (= (:type root) :frame)) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-several-shapes + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :shape1 :type :rect) + (ths/add-sample-shape :shape2 :type :rect)) + + page (thf/current-page file) + shape1 (ths/get-shape file :shape1) + shape2 (ths/get-shape file :shape2) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [shape1 shape2] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + shape1' (ths/get-shape file' :shape1) + shape2' (ths/get-shape file' :shape2)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? shape1')) + (t/is (some? shape2')) + (t/is (ctst/parent-of? root shape1')) + (t/is (ctst/parent-of? root shape2')) + (t/is (= (:type root) :frame)) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-several-frames + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame) + (ths/add-sample-shape :frame2 :type :frame)) + + page (thf/current-page file) + frame1 (ths/get-shape file :frame1) + frame2 (ths/get-shape file :frame2) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [frame1 frame2] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + frame1' (ths/get-shape file' :frame1) + frame2' (ths/get-shape file' :frame2)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? frame1')) + (t/is (some? frame2')) + (t/is (ctst/parent-of? root frame1')) + (t/is (ctst/parent-of? root frame2')) + (t/is (= (:type root) :frame)) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-frame-with-children + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame) + (ths/add-sample-shape :shape1 :type :rect :parent-label :frame1) + (ths/add-sample-shape :shape2 :type :rect :parent-label :frame1)) + + page (thf/current-page file) + frame1 (ths/get-shape file :frame1) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [frame1] + (:objects page) + (:id page) + (:id file) + true + nil + nil) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + frame1' (ths/get-shape file' :frame1) + shape1' (ths/get-shape file' :shape1) + shape2' (ths/get-shape file' :shape2)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? frame1')) + (t/is (= (:id root) (:id frame1'))) + (t/is (ctst/parent-of? frame1' shape1')) + (t/is (ctst/parent-of? frame1' shape2')) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-copy + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + copy1-root (ths/get-shape file :copy1-root) + + ;; ==== Action + [_ component2-id changes] + (cll/generate-add-component (pcb/empty-changes) + [copy1-root] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component2' (thc/get-component-by-id file' component2-id) + root2' (ths/get-shape-by-id file' (:main-instance-id component2')) + copy1-root' (ths/get-shape file' :copy1-root)] + + ;; ==== Check + (t/is (some? component2')) + (t/is (some? root2')) + (t/is (some? copy1-root')) + (t/is (ctst/parent-of? root2' copy1-root')) + (t/is (ctk/main-instance? root2')) + (t/is (ctk/main-instance-of? (:id root2') (:id page) component2')))) + +(t/deftest test-rename-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child + :name "Test component before")) + + component (thc/get-component file :component1) + + ;; ==== Action + changes (cll/generate-rename-component (pcb/empty-changes) + (:id component) + "Test component after" + (:data file) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component file' :component1)] + + ;; ==== Check + (t/is (= (:name component') "Test component after")))) + +(t/deftest test-duplicate-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + component (thc/get-component file :component1) + + ;; ==== Action + changes (cll/generate-duplicate-component (pcb/empty-changes) + file + (:id component) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + components' (ctkl/components-seq (:data file')) + component1' (d/seek #(= (:id %) (thi/id :component1)) components') + component2' (d/seek #(not= (:id %) (thi/id :component1)) components') + root1' (ths/get-shape-by-id file' (:main-instance-id component1')) + root2' (ths/get-shape-by-id file' (:main-instance-id component2')) + child1' (ths/get-shape-by-id file' (first (:shapes root1'))) + child2' (ths/get-shape-by-id file' (first (:shapes root2')))] + + ;; ==== Check + (t/is (= 2 (count components'))) + (t/is (some? component1')) + (t/is (some? component2')) + (t/is (some? root1')) + (t/is (some? root2')) + (t/is (= (thi/id :main1-root) (:id root1'))) + (t/is (not= (thi/id :main1-root) (:id root2'))) + (t/is (some? child1')) + (t/is (some? child2')) + (t/is (= (thi/id :main1-child) (:id child1'))) + (t/is (not= (thi/id :main1-child) (:id child2'))))) + +(t/deftest test-delete-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + root (ths/get-shape file :main1-root) + + ;; ==== Action + [_ changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id root)} + {:components-v2 true}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component1' (thc/get-component file' :component1 :include-deleted? true) + copy1-root' (ths/get-shape file' :copy1-root) + + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + + saved-objects (:objects component1') + saved-main1-root' (get saved-objects (thi/id :main1-root)) + saved-main1-child' (get saved-objects (thi/id :main1-child))] + + ;; ==== Check + (t/is (true? (:deleted component1'))) + (t/is (some? copy1-root')) + (t/is (nil? main1-root')) + (t/is (nil? main1-child')) + (t/is (some? saved-main1-root')) + (t/is (some? saved-main1-child')))) + +(t/deftest test-restore-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + root (ths/get-shape file :main1-root) + + ;; ==== Action + [_ changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id root)} + {:components-v2 true}) + + file-deleted (thf/apply-changes file changes) + page-deleted (thf/current-page file-deleted) + + changes (cll/generate-restore-component (pcb/empty-changes) + (:data file-deleted) + (thi/id :component1) + (:id file-deleted) + page-deleted + (:objects page-deleted)) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component1' (thc/get-component file' :component1 :include-deleted? false) + copy1-root' (ths/get-shape file' :copy1-root) + + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + + saved-objects' (:objects component1')] + + ;; ==== Check + (t/is (nil? (:deleted component1'))) + (t/is (some? copy1-root')) + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (ctk/main-instance? main1-root')) + (t/is (ctk/main-instance-of? (:id main1-root') (:id page) component1')) + (t/is (nil? saved-objects')))) + +(t/deftest test-instantiate-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + page (thf/current-page file) + component (thc/get-component file :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) ;; This may not be moved to generate + (pcb/with-objects (:objects page))) ;; because in some cases the objects + (:objects page) ;; not the same as those on the page + (:id file) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component file' :component1) + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id file') (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-instantiate-component-from-lib + (let [;; ==== Setup + library (-> (thf/sample-file :library1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + file (thf/sample-file :file1) + + page (thf/current-page file) + component (thc/get-component library :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page))) + (:objects page) + (:id library) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file + (:id library) library}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component library :component1) + main1-root' (ths/get-shape library :main1-root) + main1-child' (ths/get-shape library :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id library) (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-instantiate-nested-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head)) + + page (thf/current-page file) + component (thc/get-component file :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page))) + (:objects page) + (:id file) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component file' :component1) + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id file') (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-instantiate-nested-component-from-lib + (let [;; ==== Setup + library (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head)) + + file (thf/sample-file :file1) + + page (thf/current-page file) + component (thc/get-component library :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page))) + (:objects page) + (:id library) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file + (:id library) library}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component library :component1) + main1-root' (ths/get-shape library :main1-root) + main1-child' (ths/get-shape library :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id library) (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-detach-copy + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + copy1-root (ths/get-shape file :copy1-root) + + ;; ==== Action + changes (cll/generate-detach-component (pcb/empty-changes) + (:id copy1-root) + (:data file) + (:id page) + {(:id file) file}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy1-root' (ths/get-shape file' :copy1-root)] + + ;; ==== Check + (t/is (some? copy1-root')) + (t/is (not (ctk/instance-head? copy1-root'))) + (t/is (not (ctk/in-component-copy? copy1-root'))))) diff --git a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc index 331ee0e8a..f5cb012b4 100644 --- a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc +++ b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc @@ -7,44 +7,45 @@ (ns common-tests.logic.comp-remove-swap-slots-test (:require [app.common.files.changes-builder :as pcb] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] [app.common.types.component :as ctk] [app.common.uuid :as uuid] [clojure.test :as t] - [common-tests.helpers.components :as thc] - [common-tests.helpers.compositions :as tho] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi] - [common-tests.helpers.shapes :as ths])) + [cuerdas.core :as str])) (t/use-fixtures :each thi/test-fixture) - ;; Related .penpot file: common/test/cases/remove-swap-slots.penpot (defn- setup-file [] - ;; :frame-b1 [:id: 3aee2370-44e4-81c8-8004-46e56a459d70, :touched: ] - ;; :blue1 [:id: 3aee2370-44e4-81c8-8004-46e56a45fc55, :touched: #{:swap-slot-3aee2370-44e4-81c8-8004-46e56a459d75}] - ;; :green-copy [:id: 3aee2370-44e4-81c8-8004-46e56a45fc56, :touched: ] - ;; :blue-copy-in-green-copy [:id: 3aee2370-44e4-81c8-8004-46e56a4631a4, :touched: #{:swap-slot-3aee2370-44e4-81c8-8004-46e56a459d6f}] - ;; :frame-yellow [:id: 3aee2370-44e4-81c8-8004-46e56a459d73, :touched: ] - ;; :frame-green [:id: 3aee2370-44e4-81c8-8004-46e56a459d6c, :touched: ] - ;; :red-copy-green [:id: 3aee2370-44e4-81c8-8004-46e56a459d6f, :touched: ] - ;; :frame-blue [:id: 3aee2370-44e4-81c8-8004-46e56a459d69, :touched: ] - ;; :frame-b2 [:id: 3aee2370-44e4-81c8-8004-46e56a4631a5, :touched: ] - ;; :frame-red [:id: 3aee2370-44e4-81c8-8004-46e56a459d66, :touched: ] - + ;; {:frame-red} [:name Frame1] # [Component :red] + ;; {:frame-blue} [:name Frame1] # [Component :blue] + ;; {:frame-green} [:name Frame1] # [Component :green] + ;; :red-copy-green [:name Frame1] @--> :frame-red + ;; {:frame-b1} [:name Frame1] # [Component :b1] + ;; :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue + ;; :frame-yellow [:name Frame1] + ;; :green-copy [:name Frame1] @--> :frame-green + ;; :blue-copy-in-green-copy [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue + ;; {:frame-b2} [:name Frame1] # [Component :b2] (-> (thf/sample-file :file1) (tho/add-frame :frame-red) (thc/make-component :red :frame-red) - (tho/add-frame :frame-blue) + (tho/add-frame :frame-blue :name "frame-blue") (thc/make-component :blue :frame-blue) (tho/add-frame :frame-green) (thc/make-component :green :frame-green) (thc/instantiate-component :red :red-copy-green :parent-label :frame-green) (tho/add-frame :frame-b1) (thc/make-component :b1 :frame-b1) - (tho/add-frame :frame-yellow :parent-label :frame-b1) + (tho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow") (thc/instantiate-component :red :red-copy :parent-label :frame-b1) (thc/component-swap :red-copy :blue :blue1) (thc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy]) @@ -52,7 +53,7 @@ (tho/add-frame :frame-b2) (thc/make-component :b2 :frame-b2))) -(t/deftest test-keep-swap-slot-relocating-blue1-to-root +(t/deftest test-remove-swap-slot-relocating-blue1-to-root (let [;; ==== Setup file (setup-file) @@ -81,7 +82,7 @@ (t/is (some? blue1')) (t/is (nil? (ctk/get-swap-slot blue1'))))) -(t/deftest test-keep-swap-slot-move-blue1-to-root +(t/deftest test-remove-swap-slot-move-blue1-to-root (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -111,7 +112,7 @@ (t/is (nil? (ctk/get-swap-slot blue1'))))) -(t/deftest test-keep-swap-slot-relocating-blue1-to-b2 +(t/deftest test-remove-swap-slot-relocating-blue1-to-b2 (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -141,7 +142,7 @@ (t/is (some? blue1')) (t/is (nil? (ctk/get-swap-slot blue1'))))) -(t/deftest test-keep-swap-slot-move-blue1-to-b2 +(t/deftest test-remove-swap-slot-move-blue1-to-b2 (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -172,7 +173,7 @@ (t/is (some? blue1')) (t/is (nil? (ctk/get-swap-slot blue1'))))) -(t/deftest test-keep-swap-slot-relocating-yellow-to-root +(t/deftest test-remove-swap-slot-relocating-yellow-to-root (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -215,7 +216,7 @@ (t/is (some? blue1'')) (t/is (nil? (ctk/get-swap-slot blue1''))))) -(t/deftest test-keep-swap-slot-move-yellow-to-root +(t/deftest test-remove-swap-slot-move-yellow-to-root (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -259,7 +260,7 @@ (t/is (nil? (ctk/get-swap-slot blue1''))))) -(t/deftest test-keep-swap-slot-relocating-yellow-to-b2 +(t/deftest test-remove-swap-slot-relocating-yellow-to-b2 (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -303,7 +304,7 @@ (t/is (some? blue1'')) (t/is (nil? (ctk/get-swap-slot blue1''))))) -(t/deftest test-keep-swap-slot-move-yellow-to-b2 +(t/deftest test-remove-swap-slot-move-yellow-to-b2 (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -347,3 +348,105 @@ ;; blue1 has not swap-id after move (t/is (some? blue1'')) (t/is (nil? (ctk/get-swap-slot blue1''))))) + +(defn- find-duplicated-shape + [original-shape page] + ;; duplicated shape has the same name, the same parent, and doesn't have a label + (->> (vals (:objects page)) + (filter #(and (= (:name %) (:name original-shape)) + (= (:parent-id %) (:parent-id original-shape)) + (str/starts-with? (thi/label (:id %)) " (pcb/empty-changes nil) + (cll/generate-duplicate-changes (:objects page) ;; objects + page ;; page + #{(:id blue1)} ;; ids + (gpt/point 0 0) ;; delta + {(:id file) file} ;; libraries + (:data file) ;; library-data + (:id file)) ;; file-id + (cll/generate-duplicate-changes-update-indices (:objects page) ;; objects + #{(:id blue1)})) ;; ids + + + + file' (thf/apply-changes file changes) + + ;; ==== Get + page' (thf/current-page file') + blue1' (ths/get-shape file' :blue1) + duplicated-blue1' (find-duplicated-shape blue1' page')] + + ;; ==== Check + + ;; blue1 has swap-id + (t/is (some? (ctk/get-swap-slot blue1'))) + + ;; duplicated-blue1 has not swap-id + (t/is (some? duplicated-blue1')) + (t/is (nil? (ctk/get-swap-slot duplicated-blue1'))))) + +(t/deftest test-remove-swap-slot-duplicate-yellow + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + yellow (ths/get-shape file :frame-yellow) + + ;; ==== Action + ;; Move blue1 into yellow + changes (cls/generate-move-shapes-to-frame (pcb/empty-changes nil) + #{(:id blue1)} ;; ids + (:id yellow) ;; frame-id + (:id page) ;; page-id + (:objects page) ;; objects + 0 ;; drop-index + nil) ;; cell + + file' (thf/apply-changes file changes) + page' (thf/current-page file') + yellow' (ths/get-shape file' :frame-yellow) + + ;; Duplicate yellow + changes' (-> (pcb/empty-changes nil) + (cll/generate-duplicate-changes (:objects page') ;; objects + page' ;; page + #{(:id yellow')} ;; ids + (gpt/point 0 0) ;; delta + {(:id file') file'} ;; libraries + (:data file') ;; library-data + (:id file')) ;; file-id + (cll/generate-duplicate-changes-update-indices (:objects page') ;; objects + #{(:id yellow')})) ;; ids + + file'' (thf/apply-changes file' changes') + + ;; ==== Get + page'' (thf/current-page file'') + blue1'' (ths/get-shape file'' :blue1) + yellow'' (ths/get-shape file'' :frame-yellow) + + + duplicated-yellow'' (find-duplicated-shape yellow'' page'') + duplicated-blue1-id'' (-> duplicated-yellow'' + :shapes + first) + duplicated-blue1'' (get (:objects page'') duplicated-blue1-id'')] + + ;; ==== Check + + ;; blue1'' has swap-id + (t/is (some? (ctk/get-swap-slot blue1''))) + + ;; duplicated-blue1'' has not swap-id + (t/is (some? duplicated-blue1'')) + (t/is (nil? (ctk/get-swap-slot duplicated-blue1''))))) diff --git a/common/test/common_tests/logic/comp_reset_test.cljc b/common/test/common_tests/logic/comp_reset_test.cljc new file mode 100644 index 000000000..d7f441ed9 --- /dev/null +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -0,0 +1,359 @@ +;; 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 INC + +(ns common-tests.logic.comp-reset-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-reset-after-changing-attribute + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-from-library + (let [;; ==== Setup + library (-> (thf/sample-file :library :is-shared true) + (tho/add-simple-component :component1 :main-root :main-child + :child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root + :library library + :children-labels [:copy-child])) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf + (:id library) library} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-adding-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate-shapes (pcb/empty-changes) + (:objects page) + #{(:parent-id copy-root)} ; parents + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 0 ; to-index + #{(thi/id :free-shape)}) ; ids + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-deleting-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]})) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not + ;; delete the child shape, but hide it (thus setting the visibility group). + [_all-parents changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id copy-child)} + {:components-v2 true}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-moving-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child1 (ths/get-shape file :copy-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate-shapes (pcb/empty-changes) + (:objects page) + #{(:parent-id copy-child1)} ; parents + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 2 ; to-index + #{(:id copy-child1)}) ; ids + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-changing-upper + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :main2-root-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + page (thf/current-page file) + copy2-root (ths/get-shape file :copy2-root) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy2-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + fills' (:fills copy2-root') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)))) + +(t/deftest test-reset-after-changing-lower + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + copy2-root (ths/get-shape file :copy2-root) + copy2-child (ths/get-shape file :copy2-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy2-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#FFFFFF")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc new file mode 100644 index 000000000..75abacea3 --- /dev/null +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -0,0 +1,492 @@ +;; 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 INC + +(ns common-tests.logic.comp-sync-test + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.shape-tree :as ctst] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-sync-when-changing-attribute + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + main-child (ths/get-shape file :main-child) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-sync-when-changing-attribute-from-library + (let [;; ==== Setup + library (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root + :library library + :children-labels [:copy-child])) + + page (thf/current-page library) + main-child (ths/get-shape library :main-child) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-library (thf/apply-changes library changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id file) + (thi/id :component1) + (:id updated-library) + {(:id updated-library) updated-library + (:id file) file} + (:id file)) + + file' (thf/apply-changes file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-sync-when-changing-attribute-preserve-touched + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + main-child (ths/get-shape file :main-child) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes1 (-> (pcb/empty-changes nil (:id page)) + (cls/generate-update-shapes + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#aaaaaa"))) + (:objects page) + {}) + (cls/generate-update-shapes + #{(:id main-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {})) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#aaaaaa")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:fill-group})))) + +(t/deftest test-sync-when-adding-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + main-root (ths/get-shape file :main-root) + + ;; ==== Action + changes1 (cls/generate-relocate-shapes (pcb/empty-changes) + (:objects page) + #{(:parent-id main-root)} ; parents + (thi/id :main-root) ; parent-id + (:id page) ; page-id + 0 ; to-index + #{(thi/id :free-shape)}) ; ids + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + main-free-shape' (ths/get-shape file' :free-shape) + copy-root' (ths/get-shape file' :copy-root) + copy-new-child-id' (d/seek #(not= % (thi/id :copy-child)) (:shapes copy-root')) + copy-new-child' (ths/get-shape-by-id file' copy-new-child-id')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-new-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-new-child') nil)) + (t/is (ctst/parent-of? copy-root' copy-new-child')) + (t/is (ctk/is-main-of? main-free-shape' copy-new-child' true)))) + +(t/deftest test-sync-when-deleting-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]})) + + page (thf/current-page file) + main-child (ths/get-shape file :main-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not + ;; delete the child shape, but hide it (thus setting the visibility group). + [_all-parents changes1] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id main-child)} + {:components-v2 true}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (nil? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (empty? (:shapes copy-root'))))) + +(t/deftest test-sync-when-moving-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :copy-root-params {:children-labels [:copy-child1 + :copy-child2 + :copy-child3]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + main-child1 (ths/get-shape file :main-child1) + + ;; ==== Action + changes1 (cls/generate-relocate-shapes (pcb/empty-changes) + (:objects page) + #{(:parent-id main-child1)} ; parents + (thi/id :main-root) ; parent-id + (:id page) ; page-id + 2 ; to-index + #{(:id main-child1)}) ; ids + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child1' (ths/get-shape file' :copy-child1) + copy-child2' (ths/get-shape file' :copy-child2) + copy-child3' (ths/get-shape file' :copy-child3)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child1')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child1') nil)) + (t/is (= (:touched copy-child2') nil)) + (t/is (= (:touched copy-child3') nil)) + (t/is (= (second (:shapes copy-root')) (:id copy-child1'))) + (t/is (= (first (:shapes copy-root')) (:id copy-child2'))) + (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) + +(t/deftest test-sync-when-changing-upper + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :main2-root-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + page (thf/current-page file) + main2-root (ths/get-shape file :main2-root) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main2-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component2) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + fills' (:fills copy2-root') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)))) + +(t/deftest test-sync-when-changing-lower-near + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + main2-nested-head (ths/get-shape file :main2-nested-head) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main2-nested-head)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component2) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') nil)))) + +(t/deftest test-sync-when-changing-lower-remote + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + main1-root (ths/get-shape file :main1-root) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main1-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + synced-file (thf/apply-changes updated-file changes2) + + changes3 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id synced-file) + (thi/id :component2) + (:id synced-file) + {(:id synced-file) synced-file} + (:id synced-file)) + + file' (thf/apply-changes synced-file changes3) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file diff --git a/common/test/common_tests/logic/comp_touched_test.cljc b/common/test/common_tests/logic/comp_touched_test.cljc new file mode 100644 index 000000000..a0907e37c --- /dev/null +++ b/common/test/common_tests/logic/comp_touched_test.cljc @@ -0,0 +1,330 @@ +;; 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 INC + +(ns common-tests.logic.comp-touched-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-touched-when-changing-attribute + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:fill-group})))) + +(t/deftest test-touched-from-library + (let [;; ==== Setup + library (-> (thf/sample-file :library :is-shared true) + (tho/add-simple-component :component1 :main-root :main-child + :child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root + :library library + :children-labels [:copy-child])) + + page (thf/current-page file) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:fill-group})))) + +(t/deftest test-not-touched-when-adding-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate-shapes (pcb/empty-changes) + (:objects page) + #{(:parent-id copy-root)} ; parents + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 0 ; to-index + #{(thi/id :free-shape)}) ; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-not-touched-when-deleting-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]})) + + page (thf/current-page file) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not + ;; delete the child shape, but hide it (thus setting the visibility group). + [_all-parents changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id copy-child)} + {:components-v2 true}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:visibility-group})))) + +(t/deftest test-not-touched-when-moving-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :copy-root-params {:children-labels [:copy-child1 + :copy-child2 + :copy-child3]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-child1 (ths/get-shape file :copy-child1) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate-shapes (pcb/empty-changes) + (:objects page) + #{(:parent-id copy-child1)} ; parents + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 2 ; to-index + #{(:id copy-child1)}) ; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child1' (ths/get-shape file' :copy-child1) + copy-child2' (ths/get-shape file' :copy-child2) + copy-child3' (ths/get-shape file' :copy-child3)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child1')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child1') nil)) + (t/is (= (:touched copy-child2') nil)) + (t/is (= (:touched copy-child3') nil)) + (t/is (= (first (:shapes copy-root')) (:id copy-child1'))) + (t/is (= (second (:shapes copy-root')) (:id copy-child2'))) + (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) + +(t/deftest test-touched-when-changing-upper + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :main2-root-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + page (thf/current-page file) + copy2-root (ths/get-shape file :copy2-root) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + fills' (:fills copy2-root') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') #{:fill-group})))) + +(t/deftest test-touched-when-changing-lower + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + copy2-child (ths/get-shape file :copy2-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') #{:fill-group})))) + +(t/deftest test-touched-when-changing-lower + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + copy2-child (ths/get-shape file :copy2-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') #{:fill-group})))) \ No newline at end of file diff --git a/common/test/common_tests/logic/component_creation_test.cljc b/common/test/common_tests/logic/component_creation_test.cljc deleted file mode 100644 index 13a7533b8..000000000 --- a/common/test/common_tests/logic/component_creation_test.cljc +++ /dev/null @@ -1,47 +0,0 @@ -;; 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 INC - -(ns common-tests.logic.component-creation-test - (:require - [app.common.files.changes-builder :as pcb] - [app.common.logic.libraries :as cll] - [clojure.test :as t] - [common-tests.helpers.components :as thc] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi] - [common-tests.helpers.shapes :as ths])) - -(t/use-fixtures :each thi/test-fixture) - -(t/deftest test-add-component-from-single-shape - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (ths/add-sample-shape :shape1 :type :frame)) - - page (thf/current-page file) - shape1 (ths/get-shape file :shape1) - - ;; ==== Action - [_ component-id changes] - (cll/generate-add-component (pcb/empty-changes) - [shape1] - (:objects page) - (:id page) - (:id file) - true - nil - nil) - - file' (thf/apply-changes file changes) - - ;; ==== Get - component (thc/get-component-by-id file' component-id) - root (ths/get-shape-by-id file' (:main-instance-id component))] - - ;; ==== Check - (t/is (some? component)) - (t/is (some? root)) - (t/is (= (:component-id root) (:id component))))) diff --git a/common/test/common_tests/logic/components_touched_test.cljc b/common/test/common_tests/logic/components_touched_test.cljc deleted file mode 100644 index c7bc5bd62..000000000 --- a/common/test/common_tests/logic/components_touched_test.cljc +++ /dev/null @@ -1,234 +0,0 @@ -;; 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 INC - -(ns common-tests.logic.components-touched-test - (:require - [app.common.files.changes-builder :as pcb] - [app.common.logic.shapes :as cls] - [clojure.test :as t] - [common-tests.helpers.compositions :as tho] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi] - [common-tests.helpers.shapes :as ths])) - -(t/use-fixtures :each thi/test-fixture) - -(t/deftest test-touched-when-changing-attribute - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tho/add-simple-component-with-copy :component1 - :main-root - :main-child - :copy-root - :main-child-params {:fills (ths/sample-fills-color - :fill-color "#abcdef")})) - page (thf/current-page file) - copy-root (ths/get-shape file :copy-root) - - ;; ==== Action - update-fn (fn [shape] - (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) - - changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) - (:shapes copy-root) - update-fn - (:objects page) - {}) - - file' (thf/apply-changes file changes) - - ;; ==== Get - copy-root' (ths/get-shape file' :copy-root) - copy-child' (ths/get-shape-by-id file' (first (:shapes copy-root'))) - fills' (:fills copy-child') - fill' (first fills')] - - ;; ==== Check - (t/is (= (count fills') 1)) - (t/is (= (:fill-color fill') "#fabada")) - (t/is (= (:fill-opacity fill') 1)) - (t/is (= (:touched copy-root') nil)) - (t/is (= (:touched copy-child') #{:fill-group})))) - -(t/deftest test-not-touched-when-adding-shape - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tho/add-simple-component-with-copy :component1 - :main-root - :main-child - :copy-root) - (ths/add-sample-shape :free-shape)) - - page (thf/current-page file) - copy-root (ths/get-shape file :copy-root) - - ;; ==== Action - - ;; IMPORTANT: as modifying copies structure is now forbidden, this action - ;; will not have any effect, and so the parent shape won't also be touched. - changes (cls/generate-relocate-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id copy-root)} ; parents - (thi/id :copy-root) ; parent-id - (:id page) ; page-id - 0 ; to-index - #{(thi/id :free-shape)}) ; ids - - file' (thf/apply-changes file changes) - - ;; ==== Get - copy-root' (ths/get-shape file' :copy-root) - copy-child' (ths/get-shape-by-id file' (first (:shapes copy-root')))] - - ;; ==== Check - (t/is (= (:touched copy-root') nil)) - (t/is (= (:touched copy-child') nil)))) - -(t/deftest test-touched-when-deleting-shape - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tho/add-simple-component-with-copy :component1 - :main-root - :main-child - :copy-root)) - - page (thf/current-page file) - copy-root (ths/get-shape file :copy-root) - - ;; ==== Action - - ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not - ;; delete the child shape, but hide it (thus setting the visibility group). - [_all-parents changes] - (cls/generate-delete-shapes (pcb/empty-changes) - file - page - (:objects page) - (set (:shapes copy-root)) - {:components-v2 true}) - - file' (thf/apply-changes file changes) - - ;; ==== Get - copy-root' (ths/get-shape file' :copy-root) - copy-child' (ths/get-shape-by-id file' (first (:shapes copy-root')))] - - ;; ==== Check - (t/is (= (:touched copy-root') nil)) - (t/is (= (:touched copy-child') #{:visibility-group})))) - -(t/deftest test-not-touched-when-moving-shape - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tho/add-component-with-many-children-and-copy :component1 - :main-root - [:main-child1 :main-child2 :main-child3] - :copy-root) - (ths/add-sample-shape :free-shape)) - - page (thf/current-page file) - copy-root (ths/get-shape file :copy-root) - copy-child1 (ths/get-shape-by-id file (first (:shapes copy-root))) - - ;; ==== Action - - ;; IMPORTANT: as modifying copies structure is now forbidden, this action - ;; will not have any effect, and so the parent shape won't also be touched. - changes (cls/generate-relocate-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id copy-child1)} ; parents - (thi/id :copy-root) ; parent-id - (:id page) ; page-id - 2 ; to-index - #{(:id copy-child1)}) ; ids - - file' (thf/apply-changes file changes) - - ;; ==== Get - copy-root' (ths/get-shape file' :copy-root) - copy-child' (ths/get-shape-by-id file' (first (:shapes copy-root')))] - - ;; ==== Check - (t/is (= (:touched copy-root') nil)) - (t/is (= (:touched copy-child') nil)))) - -(t/deftest test-touched-when-changing-upper - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tho/add-nested-component-with-copy :component1 - :main1-root - :main1-child - :component2 - :main2-root - :main2-nested-head - :copy2-root - :root2-params {:fills (ths/sample-fills-color - :fill-color "#abcdef")})) - page (thf/current-page file) - copy2-root (ths/get-shape file :copy2-root) - - ;; ==== Action - update-fn (fn [shape] - (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) - - changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) - #{(:id copy2-root)} - update-fn - (:objects page) - {}) - - file' (thf/apply-changes file changes) - - ;; ==== Get - copy2-root' (ths/get-shape file' :copy2-root) - fills' (:fills copy2-root') - fill' (first fills')] - - ;; ==== Check - (t/is (= (count fills') 1)) - (t/is (= (:fill-color fill') "#fabada")) - (t/is (= (:fill-opacity fill') 1)) - (t/is (= (:touched copy2-root') #{:fill-group})))) - -(t/deftest test-touched-when-changing-lower - (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tho/add-nested-component-with-copy :component1 - :main1-root - :main1-child - :component2 - :main2-root - :main2-nested-head - :copy2-root - :nested-head-params {:fills (ths/sample-fills-color - :fill-color "#abcdef")})) - page (thf/current-page file) - copy2-root (ths/get-shape file :copy2-root) - - ;; ==== Action - update-fn (fn [shape] - (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) - - changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) - (:shapes copy2-root) - update-fn - (:objects page) - {}) - - file' (thf/apply-changes file changes) - - ;; ==== Get - copy2-root' (ths/get-shape file' :copy2-root) - copy2-child' (ths/get-shape-by-id file' (first (:shapes copy2-root'))) - fills' (:fills copy2-child') - fill' (first fills')] - - ;; ==== Check - (t/is (= (count fills') 1)) - (t/is (= (:fill-color fill') "#fabada")) - (t/is (= (:fill-opacity fill') 1)) - (t/is (= (:touched copy2-root') nil)) - (t/is (= (:touched copy2-child') #{:fill-group})))) \ No newline at end of file diff --git a/common/test/common_tests/logic/swap_and_reset_test.cljc b/common/test/common_tests/logic/swap_and_reset_test.cljc index 40d3c7ef8..c27a5b99e 100644 --- a/common/test/common_tests/logic/swap_and_reset_test.cljc +++ b/common/test/common_tests/logic/swap_and_reset_test.cljc @@ -8,14 +8,14 @@ (:require [app.common.files.changes-builder :as pcb] [app.common.logic.libraries :as cll] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] [app.common.types.component :as ctk] [app.common.types.file :as ctf] - [clojure.test :as t] - [common-tests.helpers.components :as thc] - [common-tests.helpers.compositions :as tho] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi] - [common-tests.helpers.shapes :as ths])) + [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) diff --git a/common/test/common_tests/types/types_libraries_test.cljc b/common/test/common_tests/types/types_libraries_test.cljc index 744a39518..ab13cf868 100644 --- a/common/test/common_tests/types/types_libraries_test.cljc +++ b/common/test/common_tests/types/types_libraries_test.cljc @@ -7,6 +7,11 @@ (ns common-tests.types.types-libraries-test (:require [app.common.data :as d] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] [app.common.text :as txt] [app.common.types.colors-list :as ctcl] [app.common.types.component :as ctk] @@ -14,12 +19,7 @@ [app.common.types.file :as ctf] [app.common.types.pages-list :as ctpl] [app.common.types.typographies-list :as ctyl] - [clojure.test :as t] - [common-tests.helpers.components :as thc] - [common-tests.helpers.compositions :as tho] - [common-tests.helpers.files :as thf] - [common-tests.helpers.ids-map :as thi] - [common-tests.helpers.shapes :as ths])) + [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js index 08a372946..4c8b68424 100644 --- a/frontend/playwright.config.js +++ b/frontend/playwright.config.js @@ -18,18 +18,18 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://0.0.0.0:3500", + baseURL: "http://localhost:3000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", - locale: "en-US" + locale: "en-US", }, /* Configure projects for major browsers */ @@ -42,8 +42,9 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { + timeout: 2 * 60 * 1000, command: "yarn e2e:server", - url: "http://0.0.0.0:3500", + url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, }); diff --git a/frontend/playwright/data/dashboard/create-project.json b/frontend/playwright/data/dashboard/create-project.json new file mode 100644 index 000000000..92566a65f --- /dev/null +++ b/frontend/playwright/data/dashboard/create-project.json @@ -0,0 +1,9 @@ +{ + "~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6", + "~:created-at": "~m1715266551088", + "~:modified-at": "~m1715266551088", + "~:is-default": false, + "~:name": "New Project 1", + "~:is-pinned": false +} diff --git a/frontend/playwright/data/dashboard/get-project-files-empty.json b/frontend/playwright/data/dashboard/get-project-files-empty.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-project-files-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-project-files.json b/frontend/playwright/data/dashboard/get-project-files.json new file mode 100644 index 000000000..b0394aff1 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-project-files.json @@ -0,0 +1,20 @@ +[ + { + "~:id": "~u8b479b80-e02d-8074-8004-4088dc6bfd11", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1714045521389", + "~:modified-at": "~m1714045654874", + "~:name": "New File 2", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 1", + "~:revn": 1, + "~:is-shared": false + } +] diff --git a/frontend/playwright/data/dashboard/get-projects-new.json b/frontend/playwright/data/dashboard/get-projects-new.json new file mode 100644 index 000000000..47c85eee4 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-projects-new.json @@ -0,0 +1,18 @@ +[{ + "~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1715266551088", + "~:modified-at": "~m1715266551088", + "~:is-default": false, + "~:name": "New Project 1", + "~:is-pinned": false, + "~:count": 0 +}, +{ + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713873823633", + "~:is-default": true, + "~:name": "Drafts" +}] diff --git a/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json b/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json b/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json new file mode 100644 index 000000000..0b416835f --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json @@ -0,0 +1,26 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:onboarding-viewed": true, + "~:builtin-templates-collapsed-status": + true + } +} diff --git a/frontend/playwright/data/workspace/get-comment-threads-empty.json b/frontend/playwright/data/workspace/get-comment-threads-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/data/workspace/get-comment-threads-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-blank.json b/frontend/playwright/data/workspace/get-file-blank.json new file mode 100644 index 000000000..9e05e3b50 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-blank.json @@ -0,0 +1,58 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 1", + "~:revn": 11, + "~:modified-at": "~m1713873823633", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:is-shared": false, + "~:version": 46, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713536343369", + "~:data": { + "~:pages": [ + "~uc7ce0794-0992-8105-8004-38f28044384a" + ], + "~:pages-index": { + "~uc7ce0794-0992-8105-8004-38f28044384a": { + "~#penpot/pointer": [ + "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88", + { + "~:created-at": "~m1713873823636" + } + ] + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:options": { + "~:components-v2": true + }, + "~:recent-colors": [ + { + "~:color": "#0000ff", + "~:opacity": 1, + "~:id": null, + "~:file-id": null, + "~:image": null + } + ] + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-fragment-blank.json b/frontend/playwright/data/workspace/get-file-fragment-blank.json new file mode 100644 index 000000000..fe357c500 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-fragment-blank.json @@ -0,0 +1,97 @@ +{ + "~:id": "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88", + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:created-at": "~m1713873823631", + "~:content": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [] + } + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f28044384a", + "~:name": "Page 1" + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-libraries-empty.json b/frontend/playwright/data/workspace/get-file-libraries-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-libraries-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json b/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json new file mode 100644 index 000000000..8f55ece27 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json @@ -0,0 +1,3 @@ +{ + "c7ce0794-0992-8105-8004-38f280443849/c7ce0794-0992-8105-8004-38f28044384a/8c1035fa-01f0-8071-8004-3df966ff2c64/frame": "http://localhost:3449/assets/by-id/50d097ed-d321-4319-b00b-e82a9c9435ea" +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-font-variants-empty.json b/frontend/playwright/data/workspace/get-font-variants-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/data/workspace/get-font-variants-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-profile-for-file-comments.json b/frontend/playwright/data/workspace/get-profile-for-file-comments.json new file mode 100644 index 000000000..f11319ecf --- /dev/null +++ b/frontend/playwright/data/workspace/get-profile-for-file-comments.json @@ -0,0 +1,9 @@ +[ + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:email": "foo@example.com", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-active": true + } +] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-project-default.json b/frontend/playwright/data/workspace/get-project-default.json new file mode 100644 index 000000000..d953da8fd --- /dev/null +++ b/frontend/playwright/data/workspace/get-project-default.json @@ -0,0 +1,8 @@ +{ + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713873823633", + "~:is-default": true, + "~:name": "Drafts" +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-team-default.json b/frontend/playwright/data/workspace/get-team-default.json new file mode 100644 index 000000000..e31dcf90c --- /dev/null +++ b/frontend/playwright/data/workspace/get-team-default.json @@ -0,0 +1,23 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Default", + "~:modified-at": "~m1713533116375", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116375", + "~:is-default": true +} diff --git a/frontend/playwright/data/workspace/update-file-create-rect.json b/frontend/playwright/data/workspace/update-file-create-rect.json new file mode 100644 index 000000000..671fef98f --- /dev/null +++ b/frontend/playwright/data/workspace/update-file-create-rect.json @@ -0,0 +1,9 @@ +[ + { + "~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f", + "~:revn": 21, + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62", + "~:changes": [] + } +] diff --git a/frontend/playwright/data/workspace/ws-notifications.js b/frontend/playwright/data/workspace/ws-notifications.js new file mode 100644 index 000000000..4ab58d147 --- /dev/null +++ b/frontend/playwright/data/workspace/ws-notifications.js @@ -0,0 +1,7 @@ +export const presenceFixture = { + "~:type": "~:presence", + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:session-id": "~u37730924-d520-80f1-8004-4ae6e5c3942d", + "~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:subs-id": "~uc7ce0794-0992-8105-8004-38f280443849", +}; diff --git a/frontend/playwright/helpers/MockWebSocketHelper.js b/frontend/playwright/helpers/MockWebSocketHelper.js new file mode 100644 index 000000000..3f0f845d2 --- /dev/null +++ b/frontend/playwright/helpers/MockWebSocketHelper.js @@ -0,0 +1,74 @@ +export class MockWebSocketHelper extends EventTarget { + static #mocks = new Map(); + + static async init(page) { + await page.exposeFunction("MockWebSocket$$constructor", (url, protocols) => { + const webSocket = new MockWebSocketHelper(page, url, protocols); + this.#mocks.set(url, webSocket); + }); + await page.exposeFunction("MockWebSocket$$spyMessage", (url, data) => { + if (!this.#mocks.has(url)) { + throw new Error(`WebSocket with URL ${url} not found`); + } + this.#mocks.get(url).dispatchEvent(new MessageEvent("message", { data })); + }); + await page.exposeFunction("MockWebSocket$$spyClose", (url, code, reason) => { + if (!this.#mocks.has(url)) { + throw new Error(`WebSocket with URL ${url} not found`); + } + this.#mocks.get(url).dispatchEvent(new CloseEvent("close", { code, reason })); + }); + await page.addInitScript({ path: "playwright/scripts/MockWebSocket.js" }); + } + + static waitForURL(url) { + return new Promise((resolve) => { + const intervalID = setInterval(() => { + for (const [wsURL, ws] of this.#mocks) { + if (wsURL.includes(url)) { + clearInterval(intervalID); + return resolve(ws); + } + } + }, 30); + }); + } + + #page = null; + #url; + #protocols; + + constructor(page, url, protocols) { + super(); + this.#page = page; + this.#url = url; + this.#protocols = protocols; + } + + mockOpen(options) { + return this.#page.evaluate(({ url, options }) => { + if (typeof WebSocket.getByURL !== 'function') { + throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?') + } + WebSocket.getByURL(url).mockOpen(options); + }, { url: this.#url, options }); + } + + mockMessage(data) { + return this.#page.evaluate(({ url, data }) => { + if (typeof WebSocket.getByURL !== 'function') { + throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?') + } + WebSocket.getByURL(url).mockMessage(data); + }, { url: this.#url, data }); + } + + mockClose() { + return this.#page.evaluate(({ url }) => { + if (typeof WebSocket.getByURL !== 'function') { + throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?') + } + WebSocket.getByURL(url).mockClose(); + }, { url: this.#url }); + } +} diff --git a/frontend/playwright/helpers/index.js b/frontend/playwright/helpers/index.js deleted file mode 100644 index ac8108f81..000000000 --- a/frontend/playwright/helpers/index.js +++ /dev/null @@ -1,14 +0,0 @@ -export const interceptRPC = async (page, path, jsonFilename, options = {}) => { - const interceptConfig = { - status: 200, - ...options, - }; - - await page.route(`**/api/rpc/command/${path}`, async (route) => { - await route.fulfill({ - ...interceptConfig, - contentType: "application/transit+json", - path: `playwright/data/${jsonFilename}`, - }); - }); -}; diff --git a/frontend/playwright/helpers/intercepts.js b/frontend/playwright/helpers/intercepts.js deleted file mode 100644 index ecb46b817..000000000 --- a/frontend/playwright/helpers/intercepts.js +++ /dev/null @@ -1,8 +0,0 @@ -import { interceptRPC } from "./index"; - - -export const setupNotLogedIn = async (page) => { - await interceptRPC(page, "get-profile", "get-profile-anonymous.json"); - -}; - diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js new file mode 100644 index 000000000..b7f5e4e30 --- /dev/null +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -0,0 +1,226 @@ +window.WebSocket = class MockWebSocket extends EventTarget { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + static #mocks = new Map(); + + static getAll() { + return this.#mocks.values(); + } + + static getByURL(url) { + if (this.#mocks.has(url)) { + return this.#mocks.get(url); + } + for (const [wsURL, ws] of this.#mocks) { + if (wsURL.includes(url)) { + return ws; + } + } + return undefined; + } + + #url; + #protocols; + #protocol = ""; + #binaryType = "blob"; + #bufferedAmount = 0; + #extensions = ""; + #readyState = MockWebSocket.CONNECTING; + + #onopen = null; + #onerror = null; + #onmessage = null; + #onclose = null; + + #spyMessage = null; + #spyClose = null; + + constructor(url, protocols) { + super(); + + this.#url = url; + this.#protocols = protocols || []; + + MockWebSocket.#mocks.set(this.#url, this); + + if (typeof window["MockWebSocket$$constructor"] === "function") { + MockWebSocket$$constructor(this.#url, this.#protocols); + } + if (typeof window["MockWebSocket$$spyMessage"] === "function") { + this.#spyMessage = MockWebSocket$$spyMessage; + } + if (typeof window["MockWebSocket$$spyClose"] === "function") { + this.#spyClose = MockWebSocket$$spyClose; + } + } + + set binaryType(binaryType) { + if (!["blob", "arraybuffer"].includes(binaryType)) { + return; + } + this.#binaryType = binaryType; + } + + get binaryType() { + return this.#binaryType; + } + + get bufferedAmount() { + return this.#bufferedAmount; + } + + get extensions() { + return this.#extensions; + } + + get readyState() { + return this.#readyState; + } + + get protocol() { + return this.#protocol; + } + + get url() { + return this.#url; + } + + set onopen(callback) { + this.removeEventListener("open", this.#onopen); + this.#onopen = null; + + if (typeof callback === "function") { + this.addEventListener("open", callback); + this.#onopen = callback; + } + } + + get onopen() { + return this.#onopen; + } + + set onerror(callback) { + this.removeEventListener("error", this.#onerror); + this.#onerror = null; + + if (typeof callback === "function") { + this.addEventListener("error", callback); + this.#onerror = callback; + } + } + + get onerror() { + return this.#onerror; + } + + set onmessage(callback) { + this.removeEventListener("message", this.#onmessage); + this.#onmessage = null; + + if (typeof callback === "function") { + this.addEventListener("message", callback); + this.#onmessage = callback; + } + } + + get onmessage() { + return this.#onmessage; + } + + set onclose(callback) { + this.removeEventListener("close", this.#onclose); + this.#onclose = null; + + if (typeof callback === "function") { + this.addEventListener("close", callback); + this.#onclose = callback; + } + } + + get onclose() { + return this.#onclose; + } + + get mockProtocols() { + return this.#protocols; + } + + spyClose(callback) { + if (typeof callback !== "function") { + throw new TypeError("Invalid callback"); + } + this.#spyClose = callback; + return this; + } + + spyMessage(callback) { + if (typeof callback !== "function") { + throw new TypeError("Invalid callback"); + } + this.#spyMessage = callback; + return this; + } + + mockOpen(options) { + this.#protocol = options?.protocol || ""; + this.#extensions = options?.extensions || ""; + this.#readyState = MockWebSocket.OPEN; + this.dispatchEvent(new Event("open")); + return this; + } + + mockError(error) { + this.#readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new ErrorEvent("error", { error })); + return this; + } + + mockMessage(data) { + if (this.#readyState !== MockWebSocket.OPEN) { + throw new Error("MockWebSocket is not connected"); + } + this.dispatchEvent(new MessageEvent("message", { data })); + return this; + } + + mockClose(code, reason) { + this.#readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new CloseEvent("close", { code: code || 1000, reason: reason || "" })); + return this; + } + + send(data) { + if (this.#readyState === MockWebSocket.CONNECTING) { + throw new DOMException("InvalidStateError", "MockWebSocket is not connected"); + } + + if (this.#spyMessage) { + this.#spyMessage(this.url, data); + } + } + + close(code, reason) { + if (code && !Number.isInteger(code) && code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException("InvalidAccessError", "Invalid code"); + } + + if (reason && typeof reason === "string") { + const reasonBytes = new TextEncoder().encode(reason); + if (reasonBytes.length > 123) { + throw new DOMException("SyntaxError", "Reason is too long"); + } + } + + if ([MockWebSocket.CLOSED, MockWebSocket.CLOSING].includes(this.#readyState)) { + return; + } + + this.#readyState = MockWebSocket.CLOSING; + if (this.#spyClose) { + this.#spyClose(this.url, code, reason); + } + } +}; diff --git a/frontend/playwright/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js new file mode 100644 index 000000000..076bf13f6 --- /dev/null +++ b/frontend/playwright/ui/pages/BasePage.js @@ -0,0 +1,39 @@ +export class BasePage { + static async mockRPC(page, path, jsonFilename, options) { + if (!page) { + throw new TypeError("Invalid page argument. Must be a Playwright page."); + } + if (typeof path !== "string" && !(path instanceof RegExp)) { + throw new TypeError("Invalid path argument. Must be a string or a RegExp."); + } + + const url = typeof path === "string" ? `**/api/rpc/command/${path}` : path; + const interceptConfig = { + status: 200, + contentType: "application/transit+json", + ...options, + }; + return page.route(url, (route) => + route.fulfill({ + ...interceptConfig, + path: `playwright/data/${jsonFilename}`, + }), + ); + } + + #page = null; + + constructor(page) { + this.#page = page; + } + + get page() { + return this.#page; + } + + async mockRPC(path, jsonFilename, options) { + return BasePage.mockRPC(this.page, path, jsonFilename, options); + } +} + +export default BasePage; diff --git a/frontend/playwright/ui/pages/BaseWebSocketPage.js b/frontend/playwright/ui/pages/BaseWebSocketPage.js new file mode 100644 index 000000000..e382cd7b0 --- /dev/null +++ b/frontend/playwright/ui/pages/BaseWebSocketPage.js @@ -0,0 +1,32 @@ +import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; +import BasePage from "./BasePage"; + +export class BaseWebSocketPage extends BasePage { + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static initWebSockets(page) { + return MockWebSocketHelper.init(page); + } + + /** + * Returns a promise that resolves when a WebSocket with the given URL is created. + * + * @param {string} url + * @returns {Promise} + */ + async waitForWebSocket(url) { + return MockWebSocketHelper.waitForURL(url); + } + + /** + * + * @returns {Promise} + */ + async waitForNotificationsWebSocket() { + return this.waitForWebSocket("ws://localhost:3000/ws/notifications"); + } +} diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js new file mode 100644 index 000000000..285e47d95 --- /dev/null +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -0,0 +1,93 @@ +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class DashboardPage extends BaseWebSocketPage { + static async init(page) { + await BaseWebSocketPage.initWebSockets(page); + + await BaseWebSocketPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in-no-onboarding.json", + ); + await BaseWebSocketPage.mockRPC(page, "get-teams", "logged-in-user/get-teams-default.json"); + await BaseWebSocketPage.mockRPC( + page, + "get-font-variants?team-id=*", + "workspace/get-font-variants-empty.json", + ); + + await BaseWebSocketPage.mockRPC( + page, + "get-projects?team-id=*", + "logged-in-user/get-projects-default.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-members?team-id=*", + "logged-in-user/get-team-members-your-penpot.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-users?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-unread-comment-threads?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-recent-files?team-id=*", + "logged-in-user/get-team-recent-files-empty.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-profiles-for-file-comments", + "workspace/get-profile-for-file-comments.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-builtin-templates", + "logged-in-user/get-built-in-templates-empty.json", + ); + } + + static anyTeamId = "c7ce0794-0992-8105-8004-38e630f40f6d"; + + static draftProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; + + constructor(page) { + super(page); + this.titleLabel = page.getByRole("heading", { name: "Projects" }); + this.addProjectBtn = page.getByRole("button", { name: "+ NEW PROJECT" }); + this.projectName = page.getByText("Project 1"); + this.draftTitle = page.getByRole("heading", { name: "Drafts" }); + this.draftLink = page.getByTestId("drafts-link-sidebar"); + this.draftsFile = page.getByText(/New File 1/); + } + + async setupDraftsEmpty() { + await this.mockRPC("get-project-files?project-id=*", "dashboard/get-project-files-empty.json"); + } + + async setupDrafts() { + await this.mockRPC("get-project-files?project-id=*", "dashboard/get-project-files.json"); + } + + async setupNewProject() { + await this.mockRPC("create-project", "dashboard/create-project.json", { method: "POST" }); + await this.mockRPC("get-projects?team-id=*", "dashboard/get-projects-new.json"); + } + async goToWorkspace() { + await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/projects`); + } + + async goToDrafts() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.anyTeamId}/projects/${DashboardPage.draftProjectId}`, + ); + } +} + +export default DashboardPage; diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js new file mode 100644 index 000000000..5e94c10ca --- /dev/null +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -0,0 +1,54 @@ +import { BasePage } from "./BasePage"; + +export class LoginPage extends BasePage { + static async initWithLoggedOutUser(page) { + await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json"); + } + + constructor(page) { + super(page); + this.loginButton = page.getByRole("button", { name: "Login" }); + this.password = page.getByLabel("Password"); + this.userName = page.getByLabel("Email"); + this.invalidCredentialsError = page.getByText("Email or password is incorrect"); + this.invalidEmailError = page.getByText("Enter a valid email please"); + this.initialHeading = page.getByRole("heading", { name: "Log into my account" }); + } + + async fillEmailAndPasswordInputs(email, password) { + await this.userName.fill(email); + await this.password.fill(password); + } + + async clickLoginButton() { + await this.loginButton.click(); + } + + async setupLoggedInUser() { + await this.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); + await this.mockRPC("get-teams", "logged-in-user/get-teams-default.json"); + await this.mockRPC("get-font-variants?team-id=*", "logged-in-user/get-font-variants-empty.json"); + await this.mockRPC("get-projects?team-id=*", "logged-in-user/get-projects-default.json"); + await this.mockRPC("get-team-members?team-id=*", "logged-in-user/get-team-members-your-penpot.json"); + await this.mockRPC("get-team-users?team-id=*", "logged-in-user/get-team-users-single-user.json"); + await this.mockRPC( + "get-unread-comment-threads?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await this.mockRPC("get-team-recent-files?team-id=*", "logged-in-user/get-team-recent-files-empty.json"); + await this.mockRPC( + "get-profiles-for-file-comments", + "logged-in-user/get-profiles-for-file-comments-empty.json", + ); + } + + async setupLoginSuccess() { + await this.mockRPC("login-with-password", "logged-in-user/login-with-password-success.json"); + } + + async setupLoginError() { + await this.mockRPC("login-with-password", "login-with-password-error.json", { status: 400 }); + } +} + +export default LoginPage; diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js new file mode 100644 index 000000000..ce9b78dab --- /dev/null +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -0,0 +1,99 @@ +import { expect } from "@playwright/test"; +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class WorkspacePage extends BaseWebSocketPage { + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static async init(page) { + await BaseWebSocketPage.initWebSockets(page); + + await BaseWebSocketPage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json"); + await BaseWebSocketPage.mockRPC( + page, + "get-team-users?file-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-empty.json", + ); + await BaseWebSocketPage.mockRPC(page, "get-project?id=*", "workspace/get-project-default.json"); + await BaseWebSocketPage.mockRPC(page, "get-team?id=*", "workspace/get-team-default.json"); + await BaseWebSocketPage.mockRPC( + page, + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + } + + static anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; + static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; + static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; + + #ws = null; + + constructor(page) { + super(page); + this.pageName = page.getByTestId("page-name"); + this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); + this.viewport = page.getByTestId("viewport"); + this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`); + this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" }); + } + + async goToWorkspace() { + await this.page.goto( + `/#/workspace/${WorkspacePage.anyProjectId}/${WorkspacePage.anyFileId}?page-id=${WorkspacePage.anyPageId}`, + ); + + this.#ws = await this.waitForNotificationsWebSocket(); + await this.#ws.mockOpen(); + await this.#waitForWebSocketReadiness(); + } + + async #waitForWebSocketReadiness() { + // TODO: find a better event to settle whether the app is ready to receive notifications via ws + await expect(this.pageName).toHaveText("Page 1"); + } + + async sendPresenceMessage(fixture) { + await this.#ws.mockMessage(JSON.stringify(fixture)); + } + + async cleanUp() { + await this.#ws.mockClose(); + } + + async setupEmptyFile() { + await this.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); + await this.mockRPC("get-team-users?file-id=*", "logged-in-user/get-team-users-single-user.json"); + await this.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); + await this.mockRPC("get-project?id=*", "workspace/get-project-default.json"); + await this.mockRPC("get-team?id=*", "workspace/get-team-default.json"); + await this.mockRPC( + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json"); + await this.mockRPC( + "get-file-object-thumbnails?file-id=*", + "workspace/get-file-object-thumbnails-blank.json", + ); + await this.mockRPC("get-font-variants?team-id=*", "workspace/get-font-variants-empty.json"); + await this.mockRPC("get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json"); + await this.mockRPC("get-file-libraries?file-id=*", "workspace/get-file-libraries-empty.json"); + } + + async clickWithDragViewportAt(x, y, width, height) { + await this.page.waitForTimeout(100); + await this.viewport.hover({ position: { x, y } }); + await this.page.mouse.down(); + await this.viewport.hover({ position: { x: x + width, y: y + height } }); + await this.page.mouse.up(); + } +} diff --git a/frontend/playwright/ui/pages/login-page.js b/frontend/playwright/ui/pages/login-page.js deleted file mode 100644 index 1358f4ab3..000000000 --- a/frontend/playwright/ui/pages/login-page.js +++ /dev/null @@ -1,76 +0,0 @@ -import { interceptRPC } from "../../helpers/index"; - -class LoginPage { - constructor(page) { - this.page = page; - this.loginButton = page.getByRole("button", { name: "Login" }); - this.password = page.getByLabel("Password"); - this.userName = page.getByLabel("Email"); - this.message = page.getByText("Email or password is incorrect"); - this.badLoginMsg = page.getByText("Enter a valid email please"); - this.initialHeading = page.getByRole("heading", { name: "Log into my account" }); - } - - url() { - return this.page.url(); - } - - context() { - return this.page.context(); - } - - async fillEmailAndPasswordInputs(email, password) { - await this.userName.fill(email); - await this.password.fill(password); - } - - async clickLoginButton() { - await this.loginButton.click(); - } - - async setupAllowedUser() { - await interceptRPC(this.page, "get-profile", "logged-in-user/get-profile-logged-in.json"); - await interceptRPC(this.page, "get-teams", "logged-in-user/get-teams-default.json"); - await interceptRPC( - this.page, - "get-font-variants?team-id=*", - "logged-in-user/get-font-variants-empty.json", - ); - await interceptRPC(this.page, "get-projects?team-id=*", "logged-in-user/get-projects-default.json"); - await interceptRPC( - this.page, - "get-team-members?team-id=*", - "logged-in-user/get-team-members-your-penpot.json", - ); - await interceptRPC( - this.page, - "get-team-users?team-id=*", - "logged-in-user/get-team-users-single-user.json", - ); - await interceptRPC( - this.page, - "get-unread-comment-threads?team-id=*", - "logged-in-user/get-team-users-single-user.json", - ); - await interceptRPC( - this.page, - "get-team-recent-files?team-id=*", - "logged-in-user/get-team-recent-files-empty.json", - ); - await interceptRPC( - this.page, - "get-profiles-for-file-comments", - "logged-in-user/get-profiles-for-file-comments-empty.json", - ); - } - - async setupLoginSuccess() { - await interceptRPC(this.page, "login-with-password", "logged-in-user/login-with-password-success.json"); - } - - async setupLoginError() { - await interceptRPC(this.page, "login-with-password", "login-with-password-error.json", { status: 400 }); - } -} - -export default LoginPage; diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js new file mode 100644 index 000000000..145c1321a --- /dev/null +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -0,0 +1,44 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); +}); + +test("Dashboad page has title ", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToWorkspace(); + + await expect(dashboardPage.page).toHaveURL(/dashboard/); + await expect(dashboardPage.titleLabel).toBeVisible(); +}); + +test("User can create a new project", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupNewProject(); + + await dashboardPage.goToWorkspace(); + await dashboardPage.addProjectBtn.click(); + + await expect(dashboardPage.projectName).toBeVisible(); +}); + +test("User goes to draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDraftsEmpty(); + + await dashboardPage.goToWorkspace(); + await dashboardPage.draftLink.click(); + + await expect(dashboardPage.draftTitle).toBeVisible(); +}); + +test("User loads the draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDrafts(); + + await dashboardPage.goToDrafts(); + + await expect(dashboardPage.draftsFile).toBeVisible(); +}); diff --git a/frontend/playwright/ui/specs/login.spec.js b/frontend/playwright/ui/specs/login.spec.js index 08d3753c4..dd259cf77 100644 --- a/frontend/playwright/ui/specs/login.spec.js +++ b/frontend/playwright/ui/specs/login.spec.js @@ -1,54 +1,50 @@ import { test, expect } from "@playwright/test"; -import { setupNotLogedIn } from "../../helpers/intercepts"; - -import LoginPage from "../pages/login-page"; +import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { - await setupNotLogedIn(page); + await LoginPage.initWithLoggedOutUser(page); await page.goto("/#/auth/login"); }); -test("Shows login page when going to index and user is logged out", async ({ page }) => { +test("User is redirected to the login page when logged out", async ({ page }) => { const loginPage = new LoginPage(page); - await loginPage.setupAllowedUser(); + await loginPage.setupLoggedInUser(); - await expect(loginPage.url()).toMatch(/auth\/login$/); + await expect(loginPage.page).toHaveURL(/auth\/login$/); await expect(loginPage.initialHeading).toBeVisible(); }); -test("User submit a wrong formated email ", async ({ page }) => { - const loginPage = new LoginPage(page); +test.describe("Login form", () => { + test("User logs in by filling the login form", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + await loginPage.setupLoggedInUser(); - await loginPage.setupLoginSuccess(); + await loginPage.fillEmailAndPasswordInputs("foo@example.com", "loremipsum"); + await loginPage.clickLoginButton(); - await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + await page.waitForURL("**/dashboard/**"); + await expect(loginPage.page).toHaveURL(/dashboard/); + }); - await expect(loginPage.badLoginMsg).toBeVisible(); -}); - -test("User logs in by filling the login form", async ({ page }) => { - const loginPage = new LoginPage(page); - - await loginPage.setupLoginSuccess(); - await loginPage.setupAllowedUser(); - - await loginPage.fillEmailAndPasswordInputs("foo@example.com", "loremipsum"); - await loginPage.clickLoginButton(); - - await page.waitForURL('**/dashboard/**'); - await expect(page).toHaveURL(/dashboard/); - // await expect(loginPage.url()).toMatch(/dashboard/); -}); - -test("User submits wrong credentials", async ({ page }) => { - const loginPage = new LoginPage(page); - - await loginPage.setupLoginError(); - - await loginPage.fillEmailAndPasswordInputs("test@example.com", "loremipsum"); - await loginPage.clickLoginButton(); - - await expect(loginPage.message).toBeVisible(); - await expect(loginPage.url()).toMatch(/auth\/login$/); + test("User gets error message when submitting an bad formatted email ", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + + await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + + await expect(loginPage.invalidEmailError).toBeVisible(); + }); + + test("User gets error message when submitting wrong credentials", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginError(); + + await loginPage.fillEmailAndPasswordInputs("test@example.com", "loremipsum"); + await loginPage.clickLoginButton(); + + await expect(loginPage.invalidCredentialsError).toBeVisible(); + await expect(loginPage.page).toHaveURL(/auth\/login$/); + }); }); diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js new file mode 100644 index 000000000..3f97aad90 --- /dev/null +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -0,0 +1,40 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; +import { presenceFixture } from "../../data/workspace/ws-notifications"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +test.skip("User loads worskpace with empty file", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(page); + + await workspacePage.goToWorkspace(); + + await expect(workspacePage.pageName).toHaveText("Page 1"); +}); + +test.skip("User receives presence notifications updates in the workspace", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + + await workspacePage.goToWorkspace(); + await workspacePage.sendPresenceMessage(presenceFixture); + + await expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2); +}); + +test.skip("User draws a rect", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json"); + + await workspacePage.goToWorkspace(); + await workspacePage.rectShapeButton.click(); + await workspacePage.clickWithDragViewportAt(128, 128, 200, 100); + + const shape = await workspacePage.rootShape.locator("rect"); + expect(shape).toHaveAttribute("width", "200"); + expect(shape).toHaveAttribute("height", "100"); +}); diff --git a/frontend/scripts/e2e-server.js b/frontend/scripts/e2e-server.js index cebddcdd3..4c441d21c 100644 --- a/frontend/scripts/e2e-server.js +++ b/frontend/scripts/e2e-server.js @@ -3,7 +3,7 @@ import { fileURLToPath } from "url"; import path from "path"; const app = express(); -const port = 3500; +const port = 3000; const staticPath = path.join(fileURLToPath(import.meta.url), "../../resources/public"); app.use(express.static(staticPath)); diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index c95f72e1a..7fba68aa2 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -157,8 +157,10 @@ (defn resolve-file-media ([media] (resolve-file-media media false)) - ([{:keys [id] :as media} thumbnail?] - (dm/str - (cond-> (u/join public-uri "assets/by-file-media-id/") - (true? thumbnail?) (u/join (dm/str id "/thumbnail")) - (false? thumbnail?) (u/join (dm/str id)))))) + ([{:keys [id data-uri] :as media} thumbnail?] + (if data-uri + data-uri + (dm/str + (cond-> (u/join public-uri "assets/by-file-media-id/") + (true? thumbnail?) (u/join (dm/str id "/thumbnail")) + (false? thumbnail?) (u/join (dm/str id))))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index bb88913c4..ea52674f5 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -19,6 +19,7 @@ [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gslg] + [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] [app.common.schema :as sm] [app.common.text :as txt] @@ -1868,7 +1869,8 @@ drop-cell (when (ctl/grid-layout? all-objects parent-id) (gslg/get-drop-cell frame-id all-objects position)) - changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries ldata file-id) + changes (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes all-objects page selected delta libraries ldata file-id) (pcb/amend-changes (partial process-rchange media-idx)) (pcb/amend-changes (partial change-add-obj-index objects selected index))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index e0dee732f..f05cf5d5e 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -17,12 +17,6 @@ [app.common.logic.libraries :as cll] [app.common.record :as cr] [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.page :as ctp] - [app.common.types.shape-tree :as ctst] - [app.common.types.shape.interactions :as ctsi] - [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.events :as ev] [app.main.data.modal :as md] @@ -368,308 +362,6 @@ (rx/of (select-shape (:id selected)))))))) ;; --- Duplicate Shapes -(declare prepare-duplicate-shape-change) -(declare prepare-duplicate-flows) -(declare prepare-duplicate-guides) - -(defn prepare-duplicate-changes - "Prepare objects to duplicate: generate new id, give them unique names, - move to the desired position, and recalculate parents and frames as needed." - ([all-objects page ids delta it libraries library-data file-id] - (let [init-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/with-objects all-objects))] - (prepare-duplicate-changes all-objects page ids delta it libraries library-data file-id init-changes))) - - ([all-objects page ids delta it libraries library-data file-id init-changes] - (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (cfh/get-used-names (:objects page))) - update-unames! (fn [new-name] (vswap! unames conj new-name)) - all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) - - ;; We need ids-map for remapping the grid layout. But when duplicating the guides - ;; we calculate a new one because the components will have created new shapes. - ids-map (into {} (map #(vector % (uuid/next))) all-ids) - - changes - (->> shapes - (reduce #(prepare-duplicate-shape-change %1 - all-objects - page - unames - update-unames! - ids-map - %2 - delta - nil - libraries - library-data - it - file-id) - init-changes)) - - ;; We need to check the changes to get the ids-map - ids-map - (into {} - (comp - (filter #(= :add-obj (:type %))) - (map #(vector (:old-id %) (-> % :obj :id)))) - (:redo-changes changes))] - - (-> changes - (prepare-duplicate-flows shapes page ids-map) - (prepare-duplicate-guides shapes page ids-map delta))))) - -(defn- prepare-duplicate-component-change - [changes objects page component-root parent-id frame-id delta libraries library-data] - (let [component-id (:component-id component-root) - file-id (:component-file component-root) - main-component (ctf/get-component libraries file-id component-id) - moved-component (gsh/move component-root delta) - pos (gpt/point (:x moved-component) (:y moved-component)) - origin-frame (get-in page [:objects frame-id]) - delta (cond-> delta - (some? origin-frame) - (gpt/subtract (-> origin-frame :selrect gpt/point))) - - instantiate-component - #(cll/generate-instantiate-component changes - objects - file-id - (:component-id component-root) - pos - page - libraries - (:id component-root) - parent-id - frame-id - {}) - - restore-component - #(let [restore (cll/prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] - [(:shape restore) (:changes restore)]) - - [_shape changes] - (if (nil? main-component) - (restore-component) - (instantiate-component))] - changes)) - -;; TODO: move to common.files.shape-helpers -(defn- prepare-duplicate-shape-change - ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id] - (prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id (:frame-id obj) (:parent-id obj) false false true)) - - ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] - (cond - (nil? obj) - changes - - (ctf/is-main-of-known-component? obj libraries) - (prepare-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) - - :else - (let [frame? (cfh/frame-shape? obj) - group? (cfh/group-shape? obj) - bool? (cfh/bool-shape? obj) - new-id (ids-map (:id obj)) - parent-id (or parent-id frame-id) - parent (get objects parent-id) - name (:name obj) - - is-component-root? (or (:saved-component-root obj) - ;; Backward compatibility - (:saved-component-root? obj) - (ctk/instance-root? obj)) - duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) - is-component-main? (ctk/main-instance? obj) - subinstance-head? (ctk/subinstance-head? obj) - instance-root? (ctk/instance-root? obj) - - into-component? (and duplicating-component? - (ctn/in-any-component? objects parent)) - - level-delta (if (some? level-delta) - level-delta - (ctn/get-nesting-level-delta objects obj parent)) - new-shape-ref (ctf/advance-shape-ref nil page libraries obj level-delta {:include-deleted? true}) - - regenerate-component - (fn [changes shape] - (let [components-v2 (dm/get-in library-data [:options :components-v2]) - [_ changes] (cll/generate-add-component-changes changes shape objects file-id (:id page) components-v2)] - changes)) - - new-obj - (-> obj - (assoc :id new-id - :name name - :parent-id parent-id - :frame-id frame-id) - - (cond-> (and (not instance-root?) - subinstance-head? - remove-swap-slot?) - (ctk/remove-swap-slot)) - - (dissoc :shapes - :use-for-thumbnail) - - (cond-> (not is-component-root?) - (dissoc :main-instance)) - - (cond-> into-component? - (dissoc :component-root)) - - (cond-> (and (ctk/instance-head? obj) - (not into-component?)) - (assoc :component-root true)) - - (cond-> (or frame? group? bool?) - (assoc :shapes [])) - - (cond-> (and (some? new-shape-ref) - (not= new-shape-ref (:shape-ref obj))) - (assoc :shape-ref new-shape-ref)) - - (gsh/move delta) - (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) - - (cond-> (ctl/grid-layout? obj) - (ctl/remap-grid-cells ids-map))) - - new-obj (cond-> new-obj - (not duplicating-component?) - (ctk/detach-shape)) - - ;; We want the first added object to touch it's parent, but not subsequent children - changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) - (pcb/amend-last-change #(assoc % :old-id (:id obj))) - (cond-> (ctl/grid-layout? objects (:parent-id obj)) - (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) - (pcb/reorder-grid-children [(:parent-id obj)])))) - - changes (cond-> changes - (and is-component-root? is-component-main?) - (regenerate-component new-obj)) - - ;; This is needed for the recursive call to find the new object as parent - page' (ctst/add-shape (:id new-obj) - new-obj - {:objects objects} - (:frame-id new-obj) - (:parent-id new-obj) - nil - true)] - - (reduce (fn [changes child] - (prepare-duplicate-shape-change changes - (:objects page') - page - unames - update-unames! - ids-map - child - delta - level-delta - libraries - library-data - it - file-id - (if frame? new-id frame-id) - new-id - duplicating-component? - true - (and remove-swap-slot? - ;; only remove swap slot of children when the current shape - ;; is not a subinstance head nor a instance root - (not subinstance-head?) - (not instance-root?)))) - changes - (map (d/getf objects) (:shapes obj))))))) - -(defn- prepare-duplicate-flows - [changes shapes page ids-map] - (let [flows (-> page :options :flows) - unames (volatile! (into #{} (map :name flows))) - frames-with-flow (->> shapes - (filter #(= (:type %) :frame)) - (filter #(some? (ctp/get-frame-flow flows (:id %)))))] - (if-not (empty? frames-with-flow) - (let [update-flows (fn [flows] - (reduce - (fn [flows frame] - (let [name (cfh/generate-unique-name @unames "Flow 1") - _ (vswap! unames conj name) - new-flow {:id (uuid/next) - :name name - :starting-frame (get ids-map (:id frame))}] - (ctp/add-flow flows new-flow))) - flows - frames-with-flow))] - (pcb/update-page-option changes :flows update-flows)) - changes))) - -(defn- prepare-duplicate-guides - [changes shapes page ids-map delta] - (let [guides (get-in page [:options :guides]) - frames (->> shapes (filter cfh/frame-shape?)) - - new-guides - (reduce - (fn [g frame] - (let [new-id (ids-map (:id frame)) - new-frame (-> frame (gsh/move delta)) - - new-guides - (->> guides - (vals) - (filter #(= (:frame-id %) (:id frame))) - (map #(-> % - (assoc :id (uuid/next)) - (assoc :frame-id new-id) - (assoc :position (if (= (:axis %) :x) - (+ (:position %) (- (:x new-frame) (:x frame))) - (+ (:position %) (- (:y new-frame) (:y frame))))))))] - (cond-> g - (not-empty new-guides) - (conj (into {} (map (juxt :id identity) new-guides)))))) - guides - frames)] - (-> (pcb/with-page changes page) - (pcb/set-page-option :guides new-guides)))) - -(defn duplicate-changes-update-indices - "Updates the changes to correctly set the indexes of the duplicated objects, - depending on the index of the original object respect their parent." - [objects ids changes] - (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) - index-map (reduce (fn [index-map id] - (let [parent-id (get-in objects [id :parent-id]) - parent-index (cfh/get-position-on-parent objects id)] - (update index-map parent-id (fnil conj []) [id parent-index]))) - {} - ids) - - inc-indices - (fn [[offset result] [id index]] - [(inc offset) (conj result [id (+ index offset)])]) - - fix-indices - (fn [_ entry] - (->> entry - (sort-by second) - (reduce inc-indices [1 []]) - (second) - (into {}))) - - objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] - - (pcb/amend-changes - changes - (fn [change] - (assoc change :index (get objects-indices (:old-id change))))))) (defn clear-memorize-duplicated [] @@ -746,8 +438,9 @@ libraries (wsh/get-libraries state) library-data (wsh/get-file state file-id) - changes (->> (prepare-duplicate-changes objects page ids delta it libraries library-data file-id) - (duplicate-changes-update-indices objects ids)) + changes (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page ids delta libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects ids)) tags (or (:tags changes) #{}) diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index b3c0d513f..765973787 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -15,6 +15,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.flex-layout :as flex] [app.common.geom.shapes.grid-layout :as grid] + [app.common.logic.libraries :as cll] [app.common.types.component :as ctc] [app.common.types.modifiers :as ctm] [app.common.types.shape.layout :as ctl] @@ -339,8 +340,9 @@ selected (set shapes-by-track) changes - (->> (dwse/prepare-duplicate-changes objects page selected (gpt/point 0 0) it libraries library-data file-id) - (dwse/duplicate-changes-update-indices objects selected)) + (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page selected (gpt/point 0 0) libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects selected)) ;; Creates a map with shape-id => duplicated-shape-id ids-map diff --git a/frontend/src/app/main/ui/components/link.cljs b/frontend/src/app/main/ui/components/link.cljs index 4c48681bb..6ceee146b 100644 --- a/frontend/src/app/main/ui/components/link.cljs +++ b/frontend/src/app/main/ui/components/link.cljs @@ -12,7 +12,7 @@ (mf/defc link {::mf/wrap-props false} - [{:keys [action class data-test keyboard-action children]}] + [{:keys [action class data-test keyboard-action children data-testid]}] (let [keyboard-action (d/nilv keyboard-action action)] [:a {:on-click action :class class @@ -20,5 +20,6 @@ (when ^boolean (kbd/enter? event) (keyboard-action event))) :tab-index "0" + :data-testid data-testid :data-test data-test} children])) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 96d4c9df0..10cfd2f5c 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -783,6 +783,7 @@ [:li {:class (stl/css-case :current drafts? :sidebar-nav-item true)} [:& link {:action go-drafts + :data-testid "drafts-link-sidebar" :class (stl/css :sidebar-link) :keyboard-action go-drafts-with-key} [:span {:class (stl/css :element-title)} (tr "labels.drafts")]]] diff --git a/frontend/src/app/main/ui/workspace/presence.cljs b/frontend/src/app/main/ui/workspace/presence.cljs index cdf6c6e23..6a071fbda 100644 --- a/frontend/src/app/main/ui/workspace/presence.cljs +++ b/frontend/src/app/main/ui/workspace/presence.cljs @@ -56,7 +56,7 @@ :class (stl/css :active-users-opened) :on-click on-close :on-blur on-close} - [:ul {:class (stl/css :active-users-list)} + [:ul {:class (stl/css :active-users-list) :data-testid "active-users-list"} (for [session sessions] [:& session-widget {:color (:color session) @@ -66,7 +66,7 @@ [:button {:class (stl/css-case :active-users true) :on-click on-open} - [:ul {:class (stl/css :active-users-list)} + [:ul {:class (stl/css :active-users-list) :data-testid "active-users-list"} (when (> num-sessions 2) [:span {:class (stl/css :users-num)} (dm/str "+" (- num-sessions 2))]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index d99e9dcac..e6ee5df1a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -130,7 +130,7 @@ (mf/defc asset-section {::mf/wrap-props false} - [{:keys [children file-id title section assets-count open?]}] + [{:keys [children file-id title section assets-count icon open?]}] (let [children (-> (array/normalize-to-array children) (array/without-nils)) @@ -151,7 +151,7 @@ (mf/html [:span {:class (stl/css :title-name)} [:span {:class (stl/css :section-icon)} - [:& section-icon {:section section}]] + [:& (or icon section-icon) {:section section}]] [:span {:class (stl/css :section-name)} title] diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 975615048..39064f7c7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -144,7 +144,7 @@ :auto-focus true :default-value (:name page "")}]] [:* - [:span {:class (stl/css :page-name)} + [:span {:class (stl/css :page-name) :data-testid "page-name"} (:name page)] [:div {:class (stl/css :page-actions)} (when (and deletable? (not workspace-read-only?)) diff --git a/frontend/src/app/main/ui/workspace/tokens/core.cljs b/frontend/src/app/main/ui/workspace/tokens/core.cljs index 08b7dc988..dee160729 100644 --- a/frontend/src/app/main/ui/workspace/tokens/core.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/core.cljs @@ -9,6 +9,7 @@ [app.common.data :as d :refer [ordered-map]] [app.common.types.shape.radius :as ctsr] [app.common.types.token :as ctt] + [app.main.data.tokens :as dt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.state-helpers :as wsh] @@ -31,8 +32,27 @@ [token shapes token-attributes] (some #(token-applied? token % token-attributes) shapes)) +(defn resolve-token-value [{:keys [value] :as token}] + (if-let [int-or-double (d/parse-double value)] + int-or-double + (throw (ex-info (str "Implement token value resolve for " value) token)))) + ;; Update functions ------------------------------------------------------------ +(defn on-apply-token [{:keys [token token-type-props selected-shapes] :as _props}] + (let [{:keys [attributes on-apply on-update-shape] + :or {on-apply dt/update-token-from-attributes}} token-type-props + shape-ids (->> selected-shapes + (eduction + (remove #(tokens-applied? token % attributes)) + (map :id))) + token-value (resolve-token-value token)] + (doseq [shape selected-shapes] + (st/emit! (on-apply {:token-id (:id token) + :shape-id (:id shape) + :attributes attributes})) + (on-update-shape token-value shape-ids)))) + (defn update-shape-radius [value shape-ids] (st/emit! (dch/update-shapes shape-ids @@ -47,8 +67,19 @@ (dwt/update-dimensions shape-ids :width value) (dwt/update-dimensions shape-ids :height value))) -(defn update-layout-spacing-column [value shape-ids] - (let [selected-shapes (wsh/lookup-selected @st/state)] +(defn update-opacity [value shape-ids] + (st/emit! + (dch/update-shapes shape-ids #(assoc % :opacity value)))) + +(defn update-stroke-width + [value shape-ids] + (st/emit! + (dch/update-shapes shape-ids (fn [shape] + (when (seq (:strokes shape)) + (assoc-in shape [:strokes 0 :stroke-width] value)))))) + +(defn update-layout-spacing-column [value _shape-ids] + (let [selected-shapes (wsh/lookup-selected @st/state)] (st/emit! (dwsl/update-layout selected-shapes {:layout-gap {:column-gap value :row-gap value}})))) @@ -67,6 +98,13 @@ :modal {:key :tokens/border-radius :fields [{:label "Border Radius" :key :border-radius}]}}] + [:stroke-width + {:title "Stroke Width" + :attributes ctt/stroke-width-keys + :on-update-shape update-stroke-width + :modal {:key :tokens/stroke-width + :fields [{:label "Stroke Width" + :key :stroke-width}]}}] [:box-shadow {:title "Box Shadow" :modal {:key :tokens/box-shadow @@ -92,6 +130,8 @@ :key :numeric}]}}] [:opacity {:title "Opacity" + :attributes ctt/opacity-keys + :on-update-shape update-opacity :modal {:key :tokens/opacity :fields [{:label "Opacity" :key :opacity}]}}] diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs index 83c1ef5c2..9404c9c07 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -22,6 +22,12 @@ [properties] [:& tokens-properties-form properties]) +(mf/defc stroke-width-modal + {::mf/register modal/components + ::mf/register-as :tokens/stroke-width} + [properties] + [:& tokens-properties-form properties]) + (mf/defc box-shadow-modal {::mf/register modal/components ::mf/register-as :tokens/box-shadow} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index f8bdfe226..4a32b6370 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -7,12 +7,9 @@ (ns app.main.ui.workspace.tokens.sidebar (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] [app.main.data.modal :as modal] - [app.main.data.tokens :as dt] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.search-bar :refer [search-bar]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.tokens.common :refer [workspace-shapes]] @@ -20,31 +17,39 @@ [app.util.dom :as dom] [rumext.v2 :as mf])) -(defn on-apply-token [{:keys [token token-type-props selected-shapes] :as _props}] - (let [{:keys [attributes on-apply on-update-shape] - :or {on-apply dt/update-token-from-attributes}} token-type-props - shape-ids (->> selected-shapes - (eduction - (remove #(tokens-applied? token % attributes)) - (map :id))) - token-value (d/parse-integer (:value token))] - (doseq [shape selected-shapes] - (st/emit! (on-apply {:token-id (:id token) - :shape-id (:id shape) - :attributes attributes})) - (on-update-shape token-value shape-ids)))) - (mf/defc token-pill {::mf/wrap-props false} [{:keys [on-click token highlighted? on-context-menu]}] (let [{:keys [name value]} token] + resolved-value (try + (wtc/resolve-token-value token) + (catch js/Error _ nil))] [:div {:class (stl/css-case :token-pill true :token-pill-highlighted highlighted?) - :title (str "Token value: " value) + :token-pill-invalid (not resolved-value)) + :title (str (if resolved-value "Token value: " "Invalid token value: ") value) :on-click on-click :on-context-menu on-context-menu} name])) +(mf/defc token-section-icon + {::mf/wrap-props false} + [{:keys [type]}] + (case type + :border-radius i/corner-radius + :numeric [:span {:class (stl/css :section-text-icon)} "123"] + :boolean i/boolean-difference + :opacity [:span {:class (stl/css :section-text-icon)} "%"] + :rotation i/rotation + :spacing i/padding-extended + :string i/text-mixed + :stroke-width i/stroke-size + :typography i/text + ;; TODO: Add diagonal icon here when it's available + :dimension [:div {:style {:rotate "45deg"}} i/constraint-horizontal] + :sizing [:div {:style {:rotate "45deg"}} i/constraint-horizontal] + i/add)) + (mf/defc token-component [{:keys [type file tokens selected-shapes token-type-props]}] (let [open? (mf/use-state false) @@ -76,12 +81,16 @@ (mf/deps selected-shapes token-type-props) (fn [event token] (dom/stop-propagation event) - (on-apply-token {:token token - :token-type-props token-type-props - :selected-shapes selected-shapes}))) + (wtc/on-apply-token {:token token + :token-type-props token-type-props + :selected-shapes selected-shapes}))) tokens-count (count tokens)] [:div {:on-click on-toggle-open-click} [:& cmm/asset-section {:file-id (:id file) + :icon (mf/fnc icon-wrapper [_] + [:div {:class (stl/css :section-icon)} + [:& token-section-icon {:type type}]]) + :title title :assets-count tokens-count :open? @open?} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index 5f814bd2b..3c4c4da6c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -28,4 +28,23 @@ color: var(--button-primary-foreground-color-rest); background: var(--button-primary-background-color-rest); } + + &.token-pill-invalid { + color: var(--status-color-error-500); + opacity: 0.8; + } +} + +.section-text-icon { + font-size: $fs-12; + width: 16px; + height: 16px; + display: flex; + place-content: center; +} + +.section-icon { + margin-right: $s-4; + // Align better with the label + translate: 0px -1px; } diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index d20d35e3c..d2697e018 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -276,7 +276,7 @@ (hooks/setup-shortcuts node-editing? drawing-path? text-editing? grid-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) - [:div.viewport {:style #js {"--zoom" zoom}} + [:div.viewport {:style #js {"--zoom" zoom} :data-testid "viewport"} [:& top-bar/top-bar {:layout layout}] [:div.viewport-overlays ;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap diff --git a/frontend/test/frontend_tests/basic_shapes_test.cljs b/frontend/test/frontend_tests/basic_shapes_test.cljs new file mode 100644 index 000000000..7f3188620 --- /dev/null +++ b/frontend/test/frontend_tests/basic_shapes_test.cljs @@ -0,0 +1,49 @@ +;; 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 INC +(ns frontend-tests.basic-shapes-test + (:require + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.shapes :as cths] + [app.main.data.workspace.changes :as dch] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.state :as ths])) + +(t/deftest test-update-shape + (t/async + done + (let [;; ==== Setup + store + (ths/setup-store + (-> (cthf/sample-file :file1 :page-label :page1) + (cths/add-sample-shape :shape1))) + + ;; ==== Action + events + [(dch/update-shapes [(cthi/id :shape1)] + #(assoc % :fills + (cths/sample-fills-color :fill-color + "#fabada")))]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + shape1' (get-in new-state [:workspace-data + :pages-index + (cthi/id :page1) + :objects + (cthi/id :shape1)]) + fills' (:fills shape1') + fill' (first fills')] + + (cthf/dump-shape shape1') + + ;; ==== Check + (t/is (some? shape1')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)))))))) diff --git a/frontend/test/frontend_tests/helpers/pages.cljs b/frontend/test/frontend_tests/helpers/pages.cljs index 9289d992a..939d06a07 100644 --- a/frontend/test/frontend_tests/helpers/pages.cljs +++ b/frontend/test/frontend_tests/helpers/pages.cljs @@ -97,7 +97,7 @@ (if (empty? shapes) state (let [[group changes] - (dwg/prepare-create-group nil (:objects page) (:id page) shapes prefix true)] + (dwg/prepare-create-group (pcb/empty-changes) nil (:objects page) (:id page) shapes prefix true)] (swap! idmap assoc label (:id group)) (update state :workspace-data diff --git a/frontend/test/frontend_tests/helpers/state.cljs b/frontend/test/frontend_tests/helpers/state.cljs new file mode 100644 index 000000000..3b5fd501f --- /dev/null +++ b/frontend/test/frontend_tests/helpers/state.cljs @@ -0,0 +1,56 @@ +;; 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 INC + +(ns frontend-tests.helpers.state + (:require + [app.common.pprint :refer [pprint]] + [app.common.schema :as sm] + [app.common.test-helpers.files :as cthf] + [app.main.data.workspace.layout :as layout] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(def ^private initial-state + {:workspace-layout layout/default-layout + :workspace-global layout/default-global + :current-file-id nil + :current-page-id nil + :workspace-data nil + :workspace-libraries {} + :features/team #{"components/v2"}}) + +(defn- on-error + [cause] + (js/console.log "STORE ERROR" (.-stack cause)) + (when-let [data (some-> cause ex-data ::sm/explain)] + (pprint (sm/humanize-explain data)))) + +(defn setup-store + [file] + (let [state (-> initial-state + (assoc :current-file-id (:id file) + :current-page-id (cthf/current-page-id file) + :workspace-file (dissoc file :data) + :workspace-data (:data file))) + store (ptk/store {:state state :on-error on-error})] + store)) + +(defn run-store + [store done events completed-cb] + (let [stream (ptk/input-stream store)] + (->> stream + (rx/take-until (rx/filter #(= :the/end %) stream)) + (rx/last) + (rx/tap (fn [] + (completed-cb @store))) + (rx/subs! (fn [_] (done)) + (fn [cause] + (js/console.log "[error]:" cause)) + (fn [_] + (js/console.log "[complete]")))) + (doall (for [event events] + (ptk/emit! store event))) + (ptk/emit! store :the/end))) diff --git a/frontend/test/frontend_tests/state_components_sync_test.cljs b/frontend/test/frontend_tests/state_components_sync_test.cljs deleted file mode 100644 index 7907f4ddd..000000000 --- a/frontend/test/frontend_tests/state_components_sync_test.cljs +++ /dev/null @@ -1,1667 +0,0 @@ -;; 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 INC - -(ns frontend-tests.state-components-sync-test - (:require - [app.common.colors :as clr] - [app.main.data.workspace :as dw] - [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.shapes :as dwsh] - [app.main.data.workspace.state-helpers :as wsh] - [cljs.test :as t :include-macros true] - [frontend-tests.helpers.events :as the] - [frontend-tests.helpers.libraries :as thl] - [frontend-tests.helpers.pages :as thp] - [potok.v2.core :as ptk])) - -(t/use-fixtures :each - {:before thp/reset-idmap!}) - -;; === Test reset changes ====================== - -(t/deftest test-reset-changes - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (:id instance1))] - - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/white)) - (t/is (= (:fill-opacity shape1) 1)) - (t/is (= (:touched shape1) nil)) - - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)) - (t/is (= (:touched c-shape1) nil)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance1)) - :the/end)))) - -(t/deftest test-reset-children-add - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) - - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)))))] - - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - (dwl/reset-component (:id instance1)) - :the/end)))) - -(t/deftest test-reset-children-delete - (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/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Component 1 - ;; Rect 1 - ;; Rect 2 - ;; Component 1 #--> Component 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 2 ---> Rect 2 - ;; - ;; [Component 1] - ;; page1 / Component 1 - ;; - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)))))] - - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - (dwl/reset-component (:id instance1)) - :the/end)))) - -(t/deftest test-reset-children-move - (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/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Component 1 - ;; Rect 1 - ;; Rect 2 - ;; Rect 3 - ;; Component 1 #--> Component 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 2 ---> Rect 2 - ;; Rect 3 ---> Rect 3 - ;; - ;; [Component 1] - ;; page1 / Component 1 - ;; - (let [[[group shape1 shape2 shape3] [c-group c-shape1 c-shape2 c-shape3] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (not= (:shape-ref shape3) nil)) - - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)) - (t/is (= (:name c-shape3) "Rect 3")) - (t/is (= (:touched c-shape3) nil)) - (t/is (= (:shape-ref c-shape3) nil)))))] - - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/reset-component (:id instance1)) - :the/end)))) - -(t/deftest test-reset-from-lib - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :instance1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance2 - (thp/id :component1) - (thp/id :lib1))) - - [instance2 shape2] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (:id instance2))] - - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/white)) - (t/is (= (:fill-opacity shape1) 1)) - (t/is (= (:touched shape1) nil)) - - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)) - (t/is (= (:touched c-shape1) nil)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape2)] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance2)) - :the/end)))) - -(t/deftest test-reset-nested-upper - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) - - [instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Group - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 - ;; Group #--> Group - ;; Rect 1 @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Group] - ;; page1 / Group - ;; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) - - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance2)) - :the/end)))) - -;; (t/deftest test-reset-nested-lower-near -;; (t/async done -;; (let [state (-> thp/initial-state -;; (thp/sample-page) -;; (thp/sample-shape :shape1 :rect -;; {:name "Rect 1" -;; :fill-color clr/white -;; :fill-opacity 1}) -;; (thp/make-component :main1 :component1 -;; [(thp/id :shape1)]) -;; (thp/instantiate-component :instance1 -;; (thp/id :component1)) -;; (thp/sample-shape :shape2 :circle -;; {:name "Circle 1" -;; :fill-color clr/black -;; :fill-opacity 0}) -;; (thp/frame-shapes :frame1 -;; [(thp/id :instance1) -;; (thp/id :shape2)]) -;; (thp/make-component :instance2 :component2 -;; [(thp/id :frame1)]) -;; (thp/instantiate-component :instance2 -;; (thp/id :component2))) -;; -;; [instance2 instance1 _shape1' shape2'] -;; (thl/resolve-instance state (thp/id :instance2)) -;; -;; store (the/prepare-store state done -;; (fn [new-state] -;; ;; Expected shape tree: -;; ;; -;; ;; [Page] -;; ;; Root Frame -;; ;; Rect 1 -;; ;; Rect 1 -;; ;; Group -;; ;; Rect 1 #--> Rect 1 -;; ;; Rect 1 ---> Rect 1 -;; ;; Circle 1 -;; ;; Group #--> Group -;; ;; Rect 1 @--> Rect 1 -;; ;; Rect 1 ---> Rect 1 -;; ;; Circle 1 ---> Circle 1 -;; ;; -;; ;; [Rect 1] -;; ;; page1 / Rect 1 -;; ;; -;; ;; [Group] -;; ;; page1 / Group -;; ;; -;; (let [[[instance2 instance1 shape1 shape2] -;; [c-instance2 c-instance1 c-shape1 c-shape2] _component] -;; (thl/resolve-instance-and-main -;; new-state -;; (thp/id :instance2))] -;; -;; (t/is (= (:name instance2) "Board")) -;; (t/is (= (:touched instance2) nil)) -;; (t/is (= (:name instance1) "Rect 1")) -;; (t/is (= (:touched instance1) nil)) -;; (t/is (= (:name shape1) "Circle 1")) -;; (t/is (= (:touched shape1) nil)) -;; (t/is (= (:fill-color shape1) clr/black)) -;; (t/is (= (:fill-opacity shape1) 0)) -;; (t/is (= (:name shape2) "Rect 1")) -;; (t/is (= (:touched shape2) nil)) -;; (t/is (= (:fill-color shape2) clr/white)) -;; (t/is (= (:fill-opacity shape2) 1)) -;; -;; (t/is (= (:name c-instance2) "Board")) -;; (t/is (= (:touched c-instance2) nil)) -;; (t/is (= (:name c-instance1) "Rect 1")) -;; (t/is (= (:touched c-instance1) nil)) -;; (t/is (= (:name c-shape1) "Circle 1")) -;; (t/is (= (:touched c-shape1) nil)) -;; (t/is (= (:fill-color c-shape1) clr/black)) -;; (t/is (= (:fill-opacity c-shape1) 0)) -;; (t/is (= (:name c-shape2) "Rect 1")) -;; (t/is (= (:touched c-shape2) nil)) -;; (t/is (= (:fill-color c-shape2) clr/white)) -;; (t/is (= (:fill-opacity c-shape2) 1)))))] -;; -;; (ptk/emit! -;; store -;; (dch/update-shapes [(:id shape2')] -;; (fn [shape] -;; (merge shape {:fill-color clr/test -;; :fill-opacity 0.5}))) -;; (dwl/update-component (:id instance1)) -;; (dwl/reset-component (:id instance2)) -;; :the/end)))) - -(t/deftest test-reset-nested-lower-remote - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) - - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Group - ;; Rect 1 #--> Rect 1 - ;; Rect 1* ---> Rect 1 - ;; #{:fill-group} - ;; Circle 1 - ;; Group #--> Group - ;; Rect 1 @--> Rect 1 - ;; (remote-synced) - ;; Rect 1 ---> Rect 1 - ;; (remote-synced) - ;; Circle 1 ---> Circle 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Group] - ;; page1 / Group - ;; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) #{:fill-group})) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance2)) - (dwl/reset-component (:id instance1)) - :the/end)))) - -;; === Test update component ====================== - -(t/deftest test-update-component - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 <== (not updated) - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) - - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) - - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/white)) - (t/is (= (:fill-opacity shape3) 1)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - :the/end)))) - -(t/deftest test-update-component-and-sync - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) - - file (wsh/get-local-file state) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - [_instance2 _shape1''] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) - - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) - - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:fill-opacity shape3) 0.5)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) - -(t/deftest test-update-preserve-touched - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) - - file (wsh/get-local-file state) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - [_instance2 shape1''] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1* ---> Rect 1 - ;; #{:stroke-group} - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) - - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:stroke-width shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:stroke-width shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) - - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:stroke-width shape3) 0.2)) - (t/is (= (:touched shape3) #{:stroke-group})) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :stroke-width 0.5}))) - (dch/update-shapes [(:id shape1'')] - (fn [shape] - (merge shape {:stroke-width 0.2}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) - -(t/deftest test-update-children-add - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) - - file (wsh/get-local-file state) - - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page: Page 1] - ;; Root Frame - ;; {Rect 1} # - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 - ;; - ;; ========= Local library - ;; - ;; [Component: Rect 1] - ;; --> [Page 1] Rect 1 - ;; - (let [[[main1 shape1] - [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) - - [[instance1 shape2] - [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape3] - [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) - - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) - - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] - - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) ;; We cant't change the structure of component copies, so this operation will do nothing - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) - -(t/deftest test-update-children-delete - (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/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) - - file (wsh/get-local-file state) - - [instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Component 1 - ;; Rect 1 - ;; Rect 2 - ;; Component 1 #--> Component 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 2 ---> Rect 2 - ;; Component 1 #--> Component 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 2 ---> Rect 2 - ;; - ;; [Component 1] - ;; page1 / Component 1 - ;; - (let [[[main1 shape1 shape2] - [c-main1 c-shape1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) - - [[instance1 shape3 shape4] - [c-instance1 c-shape3 c-shape4] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape5 shape6] - [c-instance2 c-shape5 c-shape6] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:hidden shape1) true)) ;; Instance shapes are not deleted but hidden - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:hidden shape2) nil)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) - - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:hidden shape3) true)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= (:name shape4) "Rect 2")) - (t/is (= (:hidden shape4) nil)) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) - - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape5) "Rect 1")) - (t/is (= (:hidden shape5) true)) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape1))) - (t/is (= (:name shape6) "Rect 2")) - (t/is (= (:hidden shape6) nil)) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape2))) - - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape3 c-shape1)) - (t/is (= c-shape4 c-shape2)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape5 c-shape1)) - (t/is (= c-shape6 c-shape2)))))] - - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) - -(t/deftest test-update-children-move - (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/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) - - file (wsh/get-local-file state) - - [instance1 shape1' _shape2' _shape3'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Component 1 - ;; Rect 1 - ;; Rect 2 - ;; Rect 3 - ;; Component 1 #--> Component 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 2 ---> Rect 2 - ;; Rect 3 ---> Rect 3 - ;; Component 1 #--> Component 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 2 ---> Rect 2 - ;; Rect 3 ---> Rect 3 - ;; - ;; [Component 1] - ;; page1 / Component 1 - ;; - (let [[[main1 shape1 shape2 shape3] - [c-main1 c-shape1 c-shape2 c-shape3] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) - - [[instance1 shape4 shape5 shape6] - [c-instance1 c-shape4 c-shape5 c-shape6] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape7 shape8 shape9] - [c-instance2 c-shape7 c-shape8 c-shape9] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) nil)) - - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape1))) - (t/is (= (:name shape5) "Rect 2")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape2))) - (t/is (= (:name shape6) "Rect 3")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape3))) - - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape7) "Rect 1")) - (t/is (= (:touched shape7) nil)) - (t/is (= (:shape-ref shape7) (:id c-shape1))) - (t/is (= (:name shape8) "Rect 2")) - (t/is (= (:touched shape8) nil)) - (t/is (= (:shape-ref shape8) (:id c-shape2))) - (t/is (= (:name shape9) "Rect 3")) - (t/is (= (:touched shape9) nil)) - (t/is (= (:shape-ref shape9) (:id c-shape3))) - - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-shape3 shape3)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape4 c-shape4)) - (t/is (= c-shape5 c-shape5)) - (t/is (= c-shape6 c-shape6)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape7 c-shape7)) - (t/is (= c-shape8 c-shape8)) - (t/is (= c-shape9 c-shape9)))))] - - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) - -(t/deftest test-update-from-lib - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1)) - (thp/instantiate-component :instance2 - (thp/id :component1) - (thp/id :lib1))) - - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - (let [[[instance1 shape1] [c-instance1 c-shape1] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - [[instance2 shape2] [_c-instance2 _c-shape2] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/test)) - (t/is (= (:fill-opacity c-shape1) 0.5)) - (t/is (= (:touched c-shape1) nil)) - - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (thp/id :lib1)) - :the/end)))) - -(t/deftest test-update-nested-upper - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) - - file (wsh/get-local-file state) - - [instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Group - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 - ;; Group #--> Group - ;; Rect 1 @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; Group #--> Group - ;; Rect 1 @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Group] - ;; page1 / Group - ;; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) - - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] - - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) - - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/test)) - (t/is (= (:fill-opacity c-shape1) 0.5)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)) - - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:fill-opacity shape3) 0.5)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/white)) - (t/is (= (:fill-opacity shape4) 1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance2) (:id file)) - :the/end)))) - -(t/deftest test-update-nested-lower-near - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) - - file (wsh/get-local-file state) - - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Group - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 - ;; Group #--> Group - ;; Rect 1 @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; Group #--> Group - ;; Rect 1 @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Group] - ;; page1 / Group - ;; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) - - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] - - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)) - - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/black)) - (t/is (= (:fill-opacity shape3) 0)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/test)) - (t/is (= (:fill-opacity shape4) 0.5)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - (dwl/update-component-sync (:id instance2) (:id file)) - :the/end)))) - -;; (t/deftest test-update-nested-lower-remote -;; (t/async done -;; (let [state (-> thp/initial-state -;; (thp/sample-page) -;; (thp/sample-shape :shape1 :rect -;; {:name "Rect 1" -;; :fill-color clr/white -;; :fill-opacity 1}) -;; (thp/make-component :main1 :component1 -;; [(thp/id :shape1)]) -;; (thp/instantiate-component :instance1 -;; (thp/id :component1)) -;; (thp/sample-shape :shape2 :circle -;; {:name "Circle 1" -;; :fill-color clr/black -;; :fill-opacity 0}) -;; (thp/frame-shapes :frame1 -;; [(thp/id :instance1) -;; (thp/id :shape2)]) -;; (thp/make-component :main2 :component2 -;; [(thp/id :frame1)]) -;; (thp/instantiate-component :instance2 -;; (thp/id :component2)) -;; (thp/instantiate-component :instance3 -;; (thp/id :component2))) -;; -;; file (wsh/get-local-file state) -;; -;; [_instance2 instance1 _shape1' shape2'] -;; (thl/resolve-instance state (thp/id :instance2)) -;; -;; store (the/prepare-store state done -;; (fn [new-state] -;; ;; Expected shape tree: -;; ;; -;; ;; [Page] -;; ;; Root Frame -;; ;; Rect 1 -;; ;; Rect 1 -;; ;; Group -;; ;; Rect 1 #--> Rect 1 -;; ;; Rect 1 ---> Rect 1 -;; ;; Circle 1 -;; ;; Group #--> Group -;; ;; Rect 1 @--> Rect 1 -;; ;; (remote-synced) -;; ;; Rect 1 ---> Rect 1 -;; ;; (remote-synced) -;; ;; Circle 1 ---> Circle 1 -;; ;; Group #--> Group -;; ;; Rect 1 @--> Rect 1 -;; ;; Rect 1 ---> Rect 1 -;; ;; Circle 1 ---> Circle 1 -;; ;; -;; ;; [Rect 1] -;; ;; page1 / Rect 1 -;; ;; -;; ;; [Group] -;; ;; page1 / Group -;; ;; -;; (let [[[instance2 instance1 shape1 shape2] -;; [c-instance2 c-instance1 c-shape1 c-shape2] _component1] -;; (thl/resolve-instance-and-main -;; new-state -;; (thp/id :instance2)) -;; -;; [[instance4 instance3 shape3 shape4] -;; [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] -;; (thl/resolve-instance-and-main -;; new-state -;; (thp/id :instance3))] -;; -;; (t/is (= (:name instance2) "Board")) -;; (t/is (= (:touched instance2) nil)) -;; (t/is (= (:name instance1) "Rect 1")) -;; (t/is (= (:touched instance1) nil)) -;; (t/is (= (:name shape1) "Circle 1")) -;; (t/is (= (:touched shape1) nil)) -;; (t/is (= (:fill-color shape1) clr/black)) -;; (t/is (= (:fill-opacity shape1) 0)) -;; (t/is (= (:name shape2) "Rect 1")) -;; (t/is (= (:touched shape2) nil)) -;; (t/is (= (:fill-color shape2) clr/test)) -;; (t/is (= (:fill-opacity shape2) 0.5)) -;; -;; (t/is (= (:name c-instance2) "Board")) -;; (t/is (= (:touched c-instance2) nil)) -;; (t/is (= (:name c-instance1) "Rect 1")) -;; (t/is (= (:touched c-instance1) nil)) -;; (t/is (= (:name c-shape1) "Circle 1")) -;; (t/is (= (:touched c-shape1) nil)) -;; (t/is (= (:fill-color c-shape1) clr/black)) -;; (t/is (= (:fill-opacity c-shape1) 0)) -;; (t/is (= (:name c-shape2) "Rect 1")) -;; (t/is (= (:touched c-shape2) nil)) -;; (t/is (= (:fill-color c-shape2) clr/test)) -;; (t/is (= (:fill-opacity c-shape2) 0.5)) -;; -;; (t/is (= (:name instance4) "Board")) -;; (t/is (= (:touched instance4) nil)) -;; (t/is (= (:name instance3) "Rect 1")) -;; (t/is (= (:touched instance3) nil)) -;; (t/is (= (:name shape3) "Circle 1")) -;; (t/is (= (:touched shape3) nil)) -;; (t/is (= (:fill-color shape3) clr/black)) -;; (t/is (= (:fill-opacity shape3) 0)) -;; (t/is (= (:name shape4) "Rect 1")) -;; (t/is (= (:touched shape4) nil)) -;; (t/is (= (:fill-color shape4) clr/test)) -;; (t/is (= (:fill-opacity shape4) 0.5)))))] -;; -;; (ptk/emit! -;; store -;; (dch/update-shapes [(:id shape2')] -;; (fn [shape] -;; (merge shape {:fill-color clr/test -;; :fill-opacity 0.5}))) -;; (dwl/update-component-sync (:id instance1) (:id file)) -;; :the/end)))) diff --git a/frontend/test/frontend_tests/state_components_test.cljs b/frontend/test/frontend_tests/state_components_test.cljs deleted file mode 100644 index 3a8751019..000000000 --- a/frontend/test/frontend_tests/state_components_test.cljs +++ /dev/null @@ -1,860 +0,0 @@ -(ns frontend-tests.state-components-test - (:require - [app.common.geom.point :as gpt] - [app.common.types.components-list :as ctkl] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.main.data.workspace :as dw] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.shapes :as dwsh] - [app.main.data.workspace.state-helpers :as wsh] - [cljs.test :as t :include-macros true] - [frontend-tests.helpers.events :as the] - [frontend-tests.helpers.libraries :as thl] - [frontend-tests.helpers.pages :as thp] - [linked.core :as lks] - [potok.v2.core :as ptk])) - -(t/use-fixtures :each - {:before thp/reset-idmap!}) - -(t/deftest test-add-component-from-single-shape - (t/testing "test-add-component-from-single-shape" - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"})) - - store (the/prepare-store state done - (fn [new-state] - ;; Uncomment to debug - ;; (ctf/dump-tree (get new-state :workspace-data) - ;; (get new-state :current-page-id) - ;; (get new-state :workspace-libraries) - ;; false true) - - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Rect 1] - ;; Rect 1 - ;; Rect 1 - ;; - (let [shape1 (thp/get-shape new-state :shape1) - - [[group shape1] [c-group c-shape1] component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) - - file (wsh/get-local-file new-state)] - - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name group) "Rect 1")) - (t/is (= (:name component) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-group) "Rect 1")) - - (thl/is-from-file group file))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :shape1)) - (dwl/add-component) - :the/end))))) - -(t/deftest test-add-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"})) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; [Page] - ;; Root Frame - ;; Component 1 - ;; Rect 1 - ;; Rect-2 - ;; - ;; [Component 1] - ;; page1 / Component 1 - ;; - (let [shape1 (thp/get-shape new-state :shape1) - - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) - - file (wsh/get-local-file new-state)] - - (t/is (= (:name group) "Component 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect-2")) - (t/is (= (:name component) "Component 1")) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect-2")) - - (thl/is-from-file group file))))] - - (ptk/emit! - store - (dw/select-shapes (lks/set (thp/id :shape1) - (thp/id :shape2))) - (dwl/add-component) - :the/end)))) - -(t/deftest test-add-component-from-frame - (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/frame-shapes :frame1 - [(thp/id :shape1) - (thp/id :shape2)])) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Group - ;; Rect 1 - ;; Rect-2 - ;; - ;; [Group] - ;; page1 / Group - ;; - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (thp/id :frame1)) - - file (wsh/get-local-file new-state)] - - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect-2")) - (t/is (= (:name group) "Board")) - (t/is (= (:name component) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect-2")) - (t/is (= (:name c-group) "Board")) - - (thl/is-from-file group file))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :frame1)) - (dwl/add-component) - :the/end)))) - -(t/deftest test-add-component-from-component-instance - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 (thp/id :component1))) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page: Page] - ;; Root Frame - ;; Rect 1 # - ;; Rect 1 - ;; Rect 1 # - ;; Rect 1* @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - (let [[[instance1 shape1] - [c-instance1 c-shape1] - component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1) - true) - - [[instance2 instance1' shape1'] - [c-instance2 c-instance1' c-shape1'] - component2] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] - - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name component1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - - (t/is (= (:name shape1') "Rect 1")) - (t/is (= (:name instance1') "Rect 1")) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:name component2) "Rect 1")) - (t/is (= (:name c-shape1') "Rect 1")) - (t/is (= (:name c-instance1') "Rect 1")) - (t/is (= (:name c-instance2) "Rect 1")))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :instance1)) - (dwl/add-component) - :the/end)))) - - -(t/deftest test-add-component-from-component-main - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [file (wsh/get-local-file new-state) - components (ctkl/components file) - page (thp/current-page new-state) - shape1 (thp/get-shape new-state :shape1) - parent1 (ctn/get-shape page (:parent-id shape1)) - main1 (thp/get-shape state :main1) - [[instance1 shape1] - [c-instance1 c-shape1] - component1] - (thl/resolve-instance-and-main - new-state - (:id main1))] - ;; Creating a component from a main doesn't generate a new component - (t/is (= (count components) 1)) - - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name component1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :main1)) - (dwl/add-component) - :the/end)))) - -(t/deftest test-rename-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) - - main1 (thp/get-shape state :main1) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; - ;; [Renamed component] - ;; page1 / Rect 1 - ;; - (let [libs (wsh/get-libraries new-state) - component (ctf/get-component libs - (:component-file main1) - (:component-id main1))] - (t/is (= (:name component) - "Renamed component")))))] - - (ptk/emit! - store - (dwl/rename-component (:component-id main1) "Renamed component") - :the/end)))) - -(t/deftest test-duplicate-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) - - main1 (thp/get-shape state :main1) - component-id (:component-id main1) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [new-component-id (->> (get-in new-state - [:workspace-data - :components]) - (keys) - (filter #(not= % component-id)) - (first)) - - [[_instance1 _shape1] - [_c-instance1 _c-shape1] - _component1] - (thl/resolve-instance-and-main - new-state - (:id main1)) - - [[_c-component2 _c-shape2] - component2] - (thl/resolve-component - new-state - (:current-file-id new-state) - new-component-id)] - - (t/is (= (:name component2) "Rect 1")))))] - - (ptk/emit! - store - (dwl/duplicate-component thp/current-file-id component-id) - :the/end)))) - -(t/deftest test-delete-component - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect {:name "Rect 1"}) - (thp/make-component :main1 :component1 [(thp/id :shape1)]) - (thp/instantiate-component :instance1 (thp/id :component1))) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;;; - ;; [Page] - ;; Root Frame - ;; Rect 1 #--> ? - ;; Rect 1 ---> ? - ;;; - (let [[main1 shape1] - (thl/resolve-noninstance - new-state - (thp/id :main1)) - - [[instance1 shape2] [c-instance1 c-shape2] component1] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1)) - - file (wsh/get-local-file new-state) - component2 (ctkl/get-component file (thp/id :component1)) - component3 (ctkl/get-deleted-component file (thp/id :component1)) - - saved-objects (:objects component3) - saved-main1 (get saved-objects (:shape-ref instance1)) - saved-shape2 (get saved-objects (:shape-ref shape2))] - - (t/is (nil? main1)) - (t/is (nil? shape1)) - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (not= (:shape-ref instance1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (nil? c-instance1)) - (t/is (nil? c-shape2)) - (t/is (nil? component1)) - - (t/is (nil? component2)) - - (t/is (= (:name component3) "Rect 1")) - (t/is (= (:deleted component3) true)) - (t/is (some? (:objects component3))) - - (t/is (= (:name saved-main1) "Rect 1")) - (t/is (= (:name saved-shape2) "Rect 1")))))] - (ptk/emit! store - (dwl/delete-component {:id (thp/id :component1)}) - :the/end)))) - -(t/deftest test-restore-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 - ;; Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[instance1 shape2] [c-instance1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) - - file (wsh/get-local-file new-state) - component2 (ctkl/get-component file (thp/id :component1)) - - saved-objects (:objects component2)] - - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - - (t/is (some? component1)) - (t/is (some? component2)) - (t/is (nil? saved-objects)))))] - - (ptk/emit! - store - (dwl/delete-component {:id (thp/id :component1)}) - (dwl/restore-component thp/current-file-id (thp/id :component1)) - :the/end)))) - -(t/deftest test-instantiate-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) - - file (wsh/get-local-file state) - component-id (thp/id :component1) - main1 (thp/get-shape state :main1) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) - - [[instance1 shape2] - [c-instance1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] - - (t/is (not= (:id main1) (:id instance1))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file instance1) - thp/current-file-id)))))] - - (ptk/emit! - store - (dwl/instantiate-component (:id file) - component-id - (gpt/point 100 100)) - :the/end)))) - -(t/deftest test-instantiate-component-from-lib - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page)) - - library-id (thp/id :lib1) - component-id (thp/id :component1) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) - - [[instance1 shape2] - [c-instance1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] - - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file instance1) library-id)))))] - - (ptk/emit! - store - (dwl/instantiate-component library-id - component-id - (gpt/point 100 100)) - :the/end)))) - -(t/deftest test-detach-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - instance1 (thp/get-shape state :instance1) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[instance2 shape1] - (thl/resolve-noninstance - new-state - (:id instance1))] - - (t/is (some? instance2)) - (t/is (some? shape1)))))] - - (ptk/emit! - store - (dwl/detach-component (:id instance1)) - :the/end)))) - - - -(t/deftest test-add-nested-component-instance - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 (thp/id :component1))) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Board - ;; Rect 1 - ;; Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Board] - ;; page1 / Board - ;; - (let [instance1 (thp/get-shape new-state :instance1) - - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] - - (t/is (= (:name group) "Board")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name component) "Board")) - (t/is (= (:name c-group) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :instance1)) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) - -(t/deftest test-add-nested-component-main - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"})) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Board - ;; Rect 1 - ;; Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; - (let [file (wsh/get-local-file new-state) - components (ctkl/components file) - page (thp/current-page new-state) - - shape1 (thp/get-shape new-state :shape1) - parent1 (ctn/get-shape page (:parent-id shape1)) - - [[group shape1] - [c-group c-shape1] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1))] - - ;; Creating a component from something containing a main doesn't generate a new component - (t/is (= (count components) 1)) - - (t/is (= (:name group) "Rect 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name component) "Rect 1")) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :shape1)) - (dwl/add-component) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) - -(t/deftest test-instantiate-nested-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/make-component :main2 :component-2 - [(thp/id :main1)])) - - file (wsh/get-local-file state) - main1 (thp/get-shape state :main1) - main2 (thp/get-shape state :main2) - component-id (:component-id main2) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Rect 1 @--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) - - [[instance1 shape1 shape2] - [c-instance1 c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] - - ;; TODO: get and check the instance inside component [Rect-2] - - (t/is (not= (:id main1) (:id instance1))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")))))] - - (ptk/emit! - store - (dwl/instantiate-component (:id file) - (:component-id main2) - (gpt/point 100 100)) - :the/end)))) - -(t/deftest test-instantiate-nested-component-from-lib - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1))) - - file (wsh/get-local-file state) - library-id (thp/id :lib1) - - store (the/prepare-store state done - (fn [new-state] - ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Group - ;; Rect 1 #--> Rect 1 - ;; Rect 1 ---> Rect 1 - ;; - ;; [Group] - ;; page1 / Group - ;; - (let [instance1 (thp/get-shape new-state :instance1) - - [[group1 shape1 shape2] [c-group1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] - - (t/is (= (:name group1) "Board")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-group1) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file group1) thp/current-file-id)) - (t/is (= (:component-file shape1) library-id)) - (t/is (= (:component-file shape2) nil)) - (t/is (= (:component-file c-group1) (:id file))) - (t/is (= (:component-file c-shape1) library-id)) - (t/is (= (:component-file c-shape2) nil)))))] - - (ptk/emit! - store - (dw/select-shape (thp/id :instance1)) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..8c0dc2cbc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "penpot", + "version": "1.20.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "penpot", + "version": "1.20.0", + "license": "MPL-2.0", + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.7" + } + }, + "node_modules/@playwright/test": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", + "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "dev": true, + "dependencies": { + "playwright": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", + "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "dev": true, + "dependencies": { + "playwright-core": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", + "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 94f482998..d26aae7be 100644 --- a/package.json +++ b/package.json @@ -18,5 +18,9 @@ "lint:clj:backend": "clj-kondo --parallel=true --lint backend/src", "lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src", "lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter" + }, + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.7" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..301801ee1 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/yarn.lock b/yarn.lock index 9a4b9536a..55dc780c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,8 +5,915 @@ __metadata: version: 8 cacheKey: 10c0 +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" + dependencies: + semver: "npm:^7.3.5" + checksum: c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@playwright/test@npm:^1.43.1": + version: 1.43.1 + resolution: "@playwright/test@npm:1.43.1" + dependencies: + playwright: "npm:1.43.1" + bin: + playwright: cli.js + checksum: 6f1398c3c66657729a14d7c2d239e2f678c37610c3163b4ad1f028cbb6b88fc845cd9033a25d35436fa86d3dfcc57ecb49028c09f7aea1389c4257e4ac9124cd + languageName: node + linkType: hard + +"@types/node@npm:^20.12.7": + version: 20.12.7 + resolution: "@types/node@npm:20.12.7" + dependencies: + undici-types: "npm:~5.26.4" + checksum: dce80d63a3b91892b321af823d624995c61e39c6a223cc0ac481a44d337640cc46931d33efb3beeed75f5c85c3bda1d97cef4c5cd4ec333caf5dee59cff6eca0 + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: "npm:^4.3.4" + checksum: e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.3 + resolution: "cacache@npm:18.0.3" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: dfda92840bb371fb66b88c087c61a74544363b37a265023223a99965b16a16bbb87661fe4948718d79df6e0cc04e85e62784fbcf1832b2a5e54ff4c46fbb45b7 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.3.15 + resolution: "glob@npm:10.3.15" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.3.6" + minimatch: "npm:^9.0.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.11.0" + bin: + glob: dist/esm/bin.mjs + checksum: cda748ddc181b31b3df9548c0991800406d5cc3b3f8110e37a8751ec1e39f37cdae7d7782d5422d7df92775121cdf00599992dff22f7ff1260344843af227c2b + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.6": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.2 + resolution: "lru-cache@npm:10.2.2" + checksum: 402d31094335851220d0b00985084288136136992979d0e015f0f1697e15d1c86052d7d53ae86b614e5b058425606efffc6969a31a091085d7a2b80a8a1e26d6 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": + version: 7.1.1 + resolution: "minipass@npm:7.1.1" + checksum: fdccc2f99c31083f45f881fd1e6971d798e333e078ab3c8988fb818c470fbd5e935388ad9adb286397eba50baebf46ef8ff487c8d3f455a69c6f3efc327bdff9 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.0": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + "penpot@workspace:.": version: 0.0.0-use.local resolution: "penpot@workspace:." + dependencies: + "@playwright/test": "npm:^1.43.1" + "@types/node": "npm:^20.12.7" languageName: unknown linkType: soft + +"playwright-core@npm:1.43.1": + version: 1.43.1 + resolution: "playwright-core@npm:1.43.1" + bin: + playwright-core: cli.js + checksum: e99f087c5f2b9ab6c379945311ea6e9e90c33cefecd8f950a0716e498dfdded738d6738266af307806d7730eacda8410c7563030690b9acf80c0b268781470b6 + languageName: node + linkType: hard + +"playwright@npm:1.43.1": + version: 1.43.1 + resolution: "playwright@npm:1.43.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.43.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 7edc1e12b8f3b791c7e8d1f9c595be35c6eaf8100f9550d5e35e979aca0bc229734e65f200f2a02dc7e21630cc40c171d7b25f5f6ccf628c79e4a2d4690909ab + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5": + version: 7.6.2 + resolution: "semver@npm:7.6.2" + bin: + semver: bin/semver.js + checksum: 97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" + dependencies: + agent-base: "npm:^7.1.1" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: 4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" + dependencies: + minipass: "npm:^7.0.3" + checksum: e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard