0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Merge branch 'token-studio-develop' into simple-context-menu

This commit is contained in:
Akshay Gupta 2024-05-15 18:30:53 +05:30 committed by GitHub
commit ec5a117318
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 5120 additions and 3500 deletions

View file

@ -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"

142
.gimlet/penpot-prod.yaml Normal file
View file

@ -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

73
.github/workflows/publish.yml vendored Normal file
View file

@ -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"

4
.gitignore vendored
View file

@ -68,3 +68,7 @@
clj-profiler/
node_modules
frontend/.storybook/preview-body.html
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View file

@ -744,6 +744,7 @@
(map lookupf)
(map mk-change))
updated-shapes))))
(apply-changes-local)))))
(defn update-component

View file

@ -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]))

View file

@ -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)))))))

View file

@ -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

View file

@ -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)))
(thc/instantiate-component component2-label copy2-root-label copy2-root-params)))

View file

@ -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)) "<no-label>")
(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)) "<no-label>") "]")
(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)) "<no-label>") "] ")
"")
(or (thi/label (:shape-ref shape)) "<no-label>")))))
(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))

View file

@ -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 "<no-label #" (subs (str id) (- (count (str id)) 6)) ">")))

View file

@ -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}]

View file

@ -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]

View file

@ -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]

View file

@ -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)))

View file

@ -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')))))

View file

@ -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 %)) "<no-label")))
first))
(t/deftest test-remove-swap-slot-duplicating-blue1
(let [;; ==== Setup
file (setup-file)
page (thf/current-page file)
blue1 (ths/get-shape file :blue1)
;; ==== Action
changes (-> (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'')))))

View file

@ -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))))

View file

@ -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))))

View file

@ -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}))))

View file

@ -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)))))

View file

@ -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}))))

View file

@ -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)

View file

@ -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)

View file

@ -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,
},
});

View file

@ -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
}

View file

@ -0,0 +1 @@
[]

View file

@ -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
}
]

View file

@ -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"
}]

View file

@ -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
}
}

View file

@ -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
}
]
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
[]

View file

@ -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"
}

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,9 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:email": "foo@example.com",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-active": true
}
]

View file

@ -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"
}

View file

@ -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
}

View file

@ -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": []
}
]

View file

@ -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",
};

View file

@ -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 });
}
}

View file

@ -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}`,
});
});
};

View file

@ -1,8 +0,0 @@
import { interceptRPC } from "./index";
export const setupNotLogedIn = async (page) => {
await interceptRPC(page, "get-profile", "get-profile-anonymous.json");
};

View file

@ -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);
}
}
};

View file

@ -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;

View file

@ -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<MockWebSocketHelper>}
*/
async waitForWebSocket(url) {
return MockWebSocketHelper.waitForURL(url);
}
/**
*
* @returns {Promise<MockWebSocketHelper>}
*/
async waitForNotificationsWebSocket() {
return this.waitForWebSocket("ws://localhost:3000/ws/notifications");
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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();
});

View file

@ -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$/);
});
});

View file

@ -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");
});

View file

@ -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));

View file

@ -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)))))))

View file

@ -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)))

View file

@ -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) #{})

View file

@ -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

View file

@ -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]))

View file

@ -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")]]]

View file

@ -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))])

View file

@ -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]

View file

@ -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?))

View file

@ -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}]}}]

View file

@ -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}

View file

@ -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?}

View file

@ -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;
}

View file

@ -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

View file

@ -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))))))))

View file

@ -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

View file

@ -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)))

File diff suppressed because it is too large Load diff

View file

@ -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 #--> <Library 1> Rect 1
;; Rect 1 ---> <Library 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 #--> <Library 1> Rect 1
;; Rect 1 ---> <Library 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))))

91
package-lock.json generated Normal file
View file

@ -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
}
}
}

View file

@ -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"
}
}

77
playwright.config.ts Normal file
View file

@ -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,
// },
});

907
yarn.lock
View file

@ -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<compat/fsevents>":
version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::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