mirror of
https://github.com/penpot/penpot.git
synced 2026-07-02 12:25:42 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
c4e72fd7f9
133
.github/workflows/tests-plugin-api-suite.yml
vendored
Normal file
133
.github/workflows/tests-plugin-api-suite.yml
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
name: "CI: Plugin API Test Suite"
|
||||
|
||||
# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it
|
||||
# needs a running frontend + the plugin runtime). Two jobs:
|
||||
#
|
||||
# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the
|
||||
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
|
||||
# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API
|
||||
# binding + in-memory store; backend-result-dependent tests are skipped via the
|
||||
# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md.
|
||||
#
|
||||
# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE
|
||||
# instance. Point PENPOT_BASE_URL at a reachable instance and provide login
|
||||
# credentials via repo secrets. Manual because the CI runner has no Docker to
|
||||
# stand up a full stack.
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_url:
|
||||
description: "Penpot base URL (e.g. https://localhost:3449)"
|
||||
required: false
|
||||
default: "https://localhost:3449"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/src/app/plugins/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
api-test-suite-mocked:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
||||
name: "Run Plugin API Test Suite (mocked)"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
|
||||
- name: Build frontend bundle
|
||||
working-directory: ./frontend
|
||||
run: ./scripts/build
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Generate API surface
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter plugin-api-test-suite run gen:api
|
||||
|
||||
- name: Run API test suite (mocked)
|
||||
working-directory: ./plugins
|
||||
env:
|
||||
MOCK_BACKEND: "1"
|
||||
run: pnpm --filter plugin-api-test-suite run test:ci
|
||||
|
||||
## The following job will launch the whole suite of tests but we need
|
||||
## to have a full environment in the CI for this to work.
|
||||
|
||||
# api-test-suite-live:
|
||||
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
# name: Run Plugin API Test Suite (live)
|
||||
# runs-on: penpot-runner-02
|
||||
# container:
|
||||
# image: penpotapp/devenv:latest
|
||||
#
|
||||
# env:
|
||||
# PENPOT_BASE_URL: ${{ github.event.inputs.base_url }}
|
||||
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
|
||||
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Setup Node
|
||||
# uses: actions/setup-node@v6
|
||||
# with:
|
||||
# node-version-file: .nvmrc
|
||||
#
|
||||
# - name: Install deps
|
||||
# working-directory: ./plugins
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# pnpm install;
|
||||
#
|
||||
# - name: Install Playwright Chromium
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
||||
#
|
||||
# - name: Generate API surface
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite run gen:api
|
||||
#
|
||||
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
|
||||
# - name: Run API test suite
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite run test:ci
|
||||
8
.github/workflows/tests-plugins.yml
vendored
8
.github/workflows/tests-plugins.yml
vendored
@ -40,19 +40,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm install -r;
|
||||
|
||||
- name: Run Lint
|
||||
working-directory: ./plugins
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
org.clojure/clojure {:mvn/version "1.12.5"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-10"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-11"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@ -39,7 +39,7 @@
|
||||
metosin/reitit-core {:mvn/version "0.10.1"}
|
||||
nrepl/nrepl {:mvn/version "1.7.0"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.11"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.12"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
|
||||
@ -63,14 +63,16 @@
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.46.7"}
|
||||
software.amazon.awssdk/sts {:mvn/version "2.46.7"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.46.18"}
|
||||
software.amazon.awssdk/sts {:mvn/version "2.46.18"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.7"}
|
||||
{:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
|
||||
org.clojure/data.csv {:mvn/version "1.1.1"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||
@ -84,9 +86,7 @@
|
||||
|
||||
:test
|
||||
{:main-opts ["-m" "kaocha.runner"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
|
||||
"--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
|
||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
|
||||
|
||||
:outdated
|
||||
|
||||
@ -4,19 +4,19 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^3.4.4",
|
||||
"sax": "^1.4.1"
|
||||
"sax": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.2",
|
||||
"nodemon": "^3.1.14",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.17.0"
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
||||
|
||||
84
backend/pnpm-lock.yaml
generated
84
backend/pnpm-lock.yaml
generated
@ -12,18 +12,18 @@ importers:
|
||||
specifier: ^3.4.4
|
||||
version: 3.7.2
|
||||
sax:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.3
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
devDependencies:
|
||||
nodemon:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.11
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
ws:
|
||||
specifier: ^8.17.0
|
||||
version: 8.18.3
|
||||
specifier: ^8.21.0
|
||||
version: 8.21.0
|
||||
|
||||
packages:
|
||||
|
||||
@ -31,15 +31,17 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
brace-expansion@5.0.7:
|
||||
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@ -52,9 +54,6 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -104,14 +103,15 @@ packages:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nodemon@3.1.11:
|
||||
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
|
||||
nodemon@3.1.14:
|
||||
resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -119,8 +119,8 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
picomatch@2.3.2:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
@ -130,11 +130,12 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
sax@1.4.3:
|
||||
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
|
||||
sax@1.6.0:
|
||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -164,8 +165,8 @@ packages:
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
ws@8.18.3:
|
||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -181,16 +182,15 @@ snapshots:
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
picomatch: 2.3.2
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
brace-expansion@5.0.7:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
balanced-match: 4.0.4
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
@ -210,8 +210,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -247,20 +245,20 @@ snapshots:
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
brace-expansion: 5.0.7
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nodemon@3.1.11:
|
||||
nodemon@3.1.14:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
minimatch: 10.2.5
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.7.3
|
||||
semver: 7.8.5
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.1
|
||||
@ -268,21 +266,21 @@ snapshots:
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
picomatch: 2.3.2
|
||||
|
||||
sax@1.4.3: {}
|
||||
sax@1.6.0: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
semver@7.8.5: {}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
semver: 7.8.5
|
||||
|
||||
source-map-support@0.5.21:
|
||||
dependencies:
|
||||
@ -303,4 +301,4 @@ snapshots:
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
ws@8.18.3: {}
|
||||
ws@8.21.0: {}
|
||||
|
||||
@ -20,7 +20,13 @@
|
||||
selmer/selmer {:mvn/version "1.13.4"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
metosin/jsonista {:mvn/version "1.0.0"}
|
||||
metosin/jsonista {:mvn/version "1.0.0"
|
||||
:exclusions [com.fasterxml.jackson.core/jackson-core
|
||||
com.fasterxml.jackson.core/jackson-databind]}
|
||||
com.fasterxml.jackson.core/jackson-core {:mvn/version "2.22.0"}
|
||||
com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.22.0"}
|
||||
|
||||
|
||||
metosin/malli {:mvn/version "0.19.1"}
|
||||
|
||||
expound/expound {:mvn/version "0.9.0"}
|
||||
@ -58,7 +64,7 @@
|
||||
{org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||
thheller/shadow-cljs {:mvn/version "3.2.0"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||
com.bhauman/rebel-readline {:mvn/version "0.1.5"}
|
||||
com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
mockery/mockery {:mvn/version "0.1.4"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"devDependencies": {
|
||||
"concurrently": "^10.0.3",
|
||||
"nodemon": "^3.1.14",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.9.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
|
||||
10
common/pnpm-lock.yaml
generated
10
common/pnpm-lock.yaml
generated
@ -19,8 +19,8 @@ importers:
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
prettier:
|
||||
specifier: 3.8.4
|
||||
version: 3.8.4
|
||||
specifier: 3.9.4
|
||||
version: 3.9.4
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
@ -161,8 +161,8 @@ packages:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
prettier@3.8.4:
|
||||
resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==}
|
||||
prettier@3.9.4:
|
||||
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@ -378,7 +378,7 @@ snapshots:
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
prettier@3.8.4: {}
|
||||
prettier@3.9.4: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
|
||||
@ -355,11 +355,15 @@
|
||||
|
||||
(defn has-point?
|
||||
[shape point]
|
||||
(if (or ^boolean (cfh/path-shape? shape)
|
||||
^boolean (cfh/bool-shape? shape)
|
||||
^boolean (cfh/circle-shape? shape))
|
||||
(slow-has-point? shape point)
|
||||
(fast-has-point? shape point)))
|
||||
(let [rotation (dm/get-prop shape :rotation)]
|
||||
;; Rotated shapes don't match their axis-aligned box, so use the polygon test.
|
||||
(if (or ^boolean (cfh/path-shape? shape)
|
||||
^boolean (cfh/bool-shape? shape)
|
||||
^boolean (cfh/circle-shape? shape)
|
||||
(and (some? rotation)
|
||||
(not ^boolean (mth/almost-zero? rotation))))
|
||||
(slow-has-point? shape point)
|
||||
(fast-has-point? shape point))))
|
||||
|
||||
(defn rect-contains-shape?
|
||||
[rect shape]
|
||||
|
||||
@ -249,7 +249,7 @@
|
||||
(defn is-variant-container?
|
||||
"Check if this shape is a variant container"
|
||||
[shape]
|
||||
(:is-variant-container shape))
|
||||
(boolean (:is-variant-container shape)))
|
||||
|
||||
(defn set-touched-group
|
||||
[touched group]
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
(def schema:registry-entry
|
||||
[:map
|
||||
[:plugin-id :string]
|
||||
[:version {:optional true} :int]
|
||||
[:name :string]
|
||||
[:description {:optional true} :string]
|
||||
[:host :string]
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
(:require
|
||||
[app.common.features :as ffeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
@ -736,6 +738,55 @@
|
||||
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-comment-thread-position
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
thread-id (uuid/custom 3 1)
|
||||
frame-id (uuid/custom 4 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(t/testing "stores position and frame-id"
|
||||
(let [change {:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 10 20)}
|
||||
res (ch/process-changes data [change])]
|
||||
(t/is (= {:frame-id frame-id :position (gpt/point 10 20)}
|
||||
(get-in res [:pages-index page-id :comment-thread-positions thread-id])))))
|
||||
|
||||
(t/testing "removes the position when frame-id and position are nil"
|
||||
(let [data (ch/process-changes data [{:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 10 20)}])
|
||||
res (ch/process-changes data [{:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id nil
|
||||
:position nil}])]
|
||||
(t/is (nil? (get-in res [:pages-index page-id :comment-thread-positions thread-id])))))
|
||||
|
||||
(t/testing "builder round-trips the position through undo and redo"
|
||||
(let [data (ch/process-changes data [{:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 10 20)}])
|
||||
page (get-in data [:pages-index page-id])
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position {:id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 100 200)}))
|
||||
redone (ch/process-changes data (:redo-changes changes))
|
||||
undone (ch/process-changes redone (:undo-changes changes))]
|
||||
(t/is (= (gpt/point 100 200)
|
||||
(get-in redone [:pages-index page-id :comment-thread-positions thread-id :position])))
|
||||
(t/is (= (gpt/point 10 20)
|
||||
(get-in undone [:pages-index page-id :comment-thread-positions thread-id :position])))))))
|
||||
|
||||
(t/deftest set-plugin-data-json-encode-decode
|
||||
(let [schema ch/schema:set-plugin-data-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
|
||||
@ -254,3 +254,19 @@
|
||||
shape {:points points}]
|
||||
(t/is (true? (gint/slow-has-point? shape (pt 50 25))))
|
||||
(t/is (false? (gint/slow-has-point? shape (pt 150 25)))))))
|
||||
|
||||
(t/deftest has-point-rotated-test
|
||||
;; Diamond (a square rotated 45º); its axis-aligned x/y/width/height box does
|
||||
;; not match the rotated polygon.
|
||||
(let [points [(pt 50 0) (pt 100 50) (pt 50 100) (pt 0 50)]
|
||||
shape {:x 20 :y 20 :width 60 :height 60 :rotation 45 :points points}]
|
||||
(t/testing "point inside the polygon but outside the box is contained"
|
||||
(t/is (true? (gint/has-point? shape (pt 50 5)))))
|
||||
(t/testing "point inside the box but outside the polygon is not contained"
|
||||
(t/is (false? (gint/has-point? shape (pt 22 22)))))))
|
||||
|
||||
(t/deftest has-point-axis-aligned-test
|
||||
(let [shape {:x 10 :y 20 :width 100 :height 50 :rotation 0}]
|
||||
(t/testing "unrotated shape uses the axis-aligned box"
|
||||
(t/is (true? (gint/has-point? shape (pt 50 40))))
|
||||
(t/is (false? (gint/has-point? shape (pt 200 40)))))))
|
||||
|
||||
@ -32,7 +32,7 @@ RUN set -ex; \
|
||||
|
||||
FROM base AS setup-node
|
||||
|
||||
ENV NODE_VERSION=v24.16.0 \
|
||||
ENV NODE_VERSION=v24.18.0 \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
|
||||
RUN set -eux; \
|
||||
|
||||
@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v24.16.0 \
|
||||
NODE_VERSION=v24.18.0 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:/opt/imagick/bin:$PATH \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers
|
||||
|
||||
@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v22.21.1 \
|
||||
NODE_VERSION=v24.18.0 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:$PATH \
|
||||
PENPOT_MCP_SERVER_HOST=0.0.0.0
|
||||
|
||||
@ -39,5 +39,5 @@
|
||||
"markdown-it-anchor": "^9.0.1",
|
||||
"markdown-it-plantuml": "^1.4.1"
|
||||
},
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620"
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.5.3+sha512.7ac1c919341c213a34dc0d02afb7143c5c26ac26ee8c4782deea821b8ac64d2134a081fd8941dae6e29bbb48f58dfc2b7fbceeccc07cb2f09d219d342a4969ed",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
@ -18,7 +18,7 @@
|
||||
"generic-pool": "^3.9.0",
|
||||
"inflation": "^2.1.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"playwright": "^1.61.0",
|
||||
"playwright": "^1.61.1",
|
||||
"raw-body": "^3.0.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"undici": "^8.5.0",
|
||||
|
||||
18
exporter/pnpm-lock.yaml
generated
18
exporter/pnpm-lock.yaml
generated
@ -35,8 +35,8 @@ importers:
|
||||
specifier: ^5.11.1
|
||||
version: 5.11.1
|
||||
playwright:
|
||||
specifier: ^1.61.0
|
||||
version: 1.61.0
|
||||
specifier: ^1.61.1
|
||||
version: 1.61.1
|
||||
raw-body:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@ -330,13 +330,13 @@ packages:
|
||||
resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
playwright-core@1.61.0:
|
||||
resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==}
|
||||
playwright-core@1.61.1:
|
||||
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.61.0:
|
||||
resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==}
|
||||
playwright@1.61.1:
|
||||
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@ -714,11 +714,11 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 2.0.0
|
||||
|
||||
playwright-core@1.61.0: {}
|
||||
playwright-core@1.61.1: {}
|
||||
|
||||
playwright@1.61.0:
|
||||
playwright@1.61.1:
|
||||
dependencies:
|
||||
playwright-core: 1.61.0
|
||||
playwright-core: 1.61.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
|
||||
@ -46,6 +46,9 @@
|
||||
|
||||
:dev
|
||||
{:extra-paths ["dev"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
|
||||
:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "3.2.2"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
@ -58,18 +58,18 @@
|
||||
"@penpot/tokenscript": "link:packages/tokenscript",
|
||||
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
||||
"@penpot/ui": "link:packages/ui",
|
||||
"@playwright/test": "1.61.0",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-themes": "10.4.6",
|
||||
"@storybook/addon-vitest": "10.4.6",
|
||||
"@storybook/react-vite": "10.4.6",
|
||||
"@tokens-studio/sd-transforms": "2.0.3",
|
||||
"@types/node": "^25.9.3",
|
||||
"@types/node": "^26.0.1",
|
||||
"@vitest/browser": "4.1.9",
|
||||
"@vitest/browser-playwright": "^4.1.9",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"autoprefixer": "^10.5.2",
|
||||
"compression": "^1.8.1",
|
||||
"concurrently": "^10.0.3",
|
||||
"date-fns": "^4.4.0",
|
||||
@ -92,12 +92,12 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opentype.js": "^2.0.0",
|
||||
"p-limit": "^7.3.0",
|
||||
"playwright": "1.61.0",
|
||||
"postcss": "^8.5.15",
|
||||
"playwright": "1.61.1",
|
||||
"postcss": "^8.5.16",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"postcss-modules": "^6.0.1",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.9.4",
|
||||
"pretty-time": "^1.1.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"randomcolor": "^0.6.2",
|
||||
@ -114,15 +114,15 @@
|
||||
"source-map-support": "^0.5.21",
|
||||
"storybook": "10.4.6",
|
||||
"style-dictionary": "5.4.4",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"stylelint-plugin-logical-css": "^2.1.0",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"svg-sprite": "^2.0.4",
|
||||
"tdigest": "^0.1.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.16",
|
||||
"vite": "^8.1.0",
|
||||
"vitest": "^4.1.9",
|
||||
"wait-on": "^9.0.4",
|
||||
"watcher": "^2.3.1",
|
||||
|
||||
@ -4,12 +4,12 @@
|
||||
"description": "Penpot Draft-JS Wrapper",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
|
||||
"immutable": "^5.1.6"
|
||||
"immutable": "^5.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.17.0",
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "Simple library for handling keyboard shortcuts",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"author": "Craig Campbell",
|
||||
"license": "Apache-2.0 WITH LLVM-exception"
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@ -14,15 +14,15 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.0",
|
||||
"@babel/preset-react": "^8.0.0",
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/preset-react": "^8.0.1",
|
||||
"@storybook/react": "10.4.6",
|
||||
"@storybook/react-vite": "10.4.6",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
@ -30,7 +30,7 @@
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"storybook": "10.4.6",
|
||||
"vite-plugin-dts": "^5.0.2"
|
||||
"vite-plugin-dts": "^5.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19.2",
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{"~:total": 2}
|
||||
@ -389,7 +389,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "line-through",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2111,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae134c87eab3": {
|
||||
@ -758,7 +788,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2719,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1409412914": {
|
||||
@ -1143,7 +1203,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 488.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2707,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1488974651": {
|
||||
@ -1293,7 +1383,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2313,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae149cc8c2f1": {
|
||||
@ -1432,7 +1552,32 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2714,
|
||||
"~:fills": [],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae149cc8c2f0": {
|
||||
@ -1571,7 +1716,32 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2514,
|
||||
"~:fills": [],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae136831d5b7": {
|
||||
@ -2116,7 +2286,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2519,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1409412913": {
|
||||
@ -2368,7 +2568,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "underline",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2106,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae149fbf3112": {
|
||||
@ -2525,7 +2755,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2318,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1409412912": {
|
||||
@ -4487,7 +4747,44 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2714,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-opacity": 1,
|
||||
"~:fill-image": {
|
||||
"~:id": "~uaa0a383a-7553-808a-8006-ae13a3c575eb",
|
||||
"~:width": 100,
|
||||
"~:height": 100,
|
||||
"~:mtype": "image/jpeg",
|
||||
"~:keep-aspect-ratio": true,
|
||||
"~:name": "sample"
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae136eba4581": {
|
||||
@ -5550,7 +5847,32 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2313,
|
||||
"~:fills": [],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae148b39a12f": {
|
||||
@ -5700,7 +6022,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2514,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae145f7fe46e": {
|
||||
@ -6244,7 +6596,37 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2106,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -6258,4 +6640,4 @@
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -637,7 +637,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🔥"
|
||||
},
|
||||
@ -654,7 +659,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👩🏿\u200d🚀"
|
||||
},
|
||||
@ -671,7 +681,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👺"
|
||||
},
|
||||
@ -688,7 +703,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🚀"
|
||||
}
|
||||
@ -706,7 +726,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
},
|
||||
{
|
||||
@ -726,7 +751,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": ""
|
||||
}
|
||||
@ -744,7 +774,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
@ -2395,7 +2430,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🔥"
|
||||
},
|
||||
@ -2412,7 +2452,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👩🏿\u200d🚀"
|
||||
},
|
||||
@ -2429,7 +2474,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👺"
|
||||
},
|
||||
@ -2446,7 +2496,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🚀"
|
||||
}
|
||||
@ -2464,7 +2519,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
},
|
||||
{
|
||||
@ -2484,7 +2544,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": ""
|
||||
}
|
||||
@ -2502,7 +2567,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
@ -3433,7 +3503,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🔥"
|
||||
},
|
||||
@ -3450,7 +3525,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👩🏿\u200d🚀"
|
||||
},
|
||||
@ -3467,7 +3547,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👺"
|
||||
},
|
||||
@ -3484,7 +3569,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🚀"
|
||||
}
|
||||
@ -3502,7 +3592,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
},
|
||||
{
|
||||
@ -3522,7 +3617,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": ""
|
||||
}
|
||||
@ -3540,7 +3640,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
|
||||
@ -162,7 +162,7 @@ test("Updates canvas background", async ({ page }) => {
|
||||
|
||||
const canvasBackgroundInput = workspace.page.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
}).first();
|
||||
await canvasBackgroundInput.fill("FABADA");
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import DashboardPage from "../pages/DashboardPage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await DashboardPage.init(page);
|
||||
});
|
||||
|
||||
test("Open invite members modal from invitations section", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await dashboardPage.setupTeamInvitationsEmpty();
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
await expect(page.getByRole("button", { name: "Invite people" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Invite people" }).click();
|
||||
await expect(page.getByText("Invite members to the team")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Invite a new member by email", async ({ page }) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await dashboardPage.setupTeamInvitationsEmpty();
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"create-team-invitations",
|
||||
"dashboard/create-team-invitations.json",
|
||||
{ method: "POST" },
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
await page.getByRole("button", { name: "Invite people" }).click();
|
||||
await expect(page.getByText("Invite members to the team")).toBeVisible();
|
||||
|
||||
const emailInput = page.getByRole("textbox", { name: "Emails, comma separated" });
|
||||
await emailInput.fill("newmember@example.com");
|
||||
await emailInput.press("Enter");
|
||||
|
||||
await page.getByRole("button", { name: "Send invitation" }).click();
|
||||
|
||||
await expect(page.getByText("Invitation sent successfully")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Invite members to the team"),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Show warning when inviting an existing member", async ({ page }) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await dashboardPage.setupTeamInvitationsEmpty();
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
await page.getByRole("button", { name: "Invite people" }).click();
|
||||
await expect(page.getByText("Invite members to the team")).toBeVisible();
|
||||
|
||||
const emailInput = page.getByRole("textbox", { name: "Emails, comma separated" });
|
||||
await emailInput.fill("foo@example.com");
|
||||
await emailInput.press("Enter");
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Some members are already on the team. We'll invite the rest.",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Disable send button when all entered emails are existing members", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await dashboardPage.setupTeamInvitationsEmpty();
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
await page.getByRole("button", { name: "Invite people" }).click();
|
||||
await expect(page.getByText("Invite members to the team")).toBeVisible();
|
||||
|
||||
const emailInput = page.getByRole("textbox", { name: "Emails, comma separated" });
|
||||
await emailInput.fill("foo@example.com");
|
||||
await emailInput.press("Enter");
|
||||
|
||||
const sendButton = page.getByRole("button", { name: "Send invitation" });
|
||||
await expect(sendButton).toBeDisabled();
|
||||
});
|
||||
1170
frontend/pnpm-lock.yaml
generated
1170
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -82,7 +82,7 @@
|
||||
|
||||
(defn- load-plugin!
|
||||
[{:keys [plugin-id name version description host code icon permissions]}]
|
||||
(st/emit! (pflag/clear plugin-id)
|
||||
(st/emit! (pflag/initialize plugin-id version)
|
||||
(save-current-plugin plugin-id))
|
||||
|
||||
(let [load-plugin (unchecked-get ug/global "ɵloadPlugin")]
|
||||
|
||||
@ -72,7 +72,6 @@
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.register :as preg]
|
||||
[app.render-wasm :as wasm]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
@ -352,10 +351,8 @@
|
||||
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
rparams (rt/get-params state)
|
||||
features (features/get-enabled-features state team-id)
|
||||
;; since render-wasm/v1 can be hot-toggled by the user, we need to query it
|
||||
;; from the state with active-feature?
|
||||
render-wasm-enabled? #(features/active-feature? @st/state "render-wasm/v1")
|
||||
render-wasm-ready? #(and (render-wasm-enabled?)
|
||||
render-wasm-enabled? (features/active-feature? state "render-wasm/v1")
|
||||
render-wasm-ready? #(and render-wasm-enabled?
|
||||
wasm-state/context-initialized?
|
||||
(not @wasm-state/context-lost?))]
|
||||
|
||||
@ -368,7 +365,7 @@
|
||||
(rx/concat
|
||||
;; Fetch all essential data that should be loaded before the file
|
||||
(rx/merge
|
||||
(if ^boolean (render-wasm-enabled?)
|
||||
(if ^boolean render-wasm-enabled?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/filter true?)
|
||||
(rx/tap (fn [_]
|
||||
@ -441,6 +438,9 @@
|
||||
(rx/take 1)
|
||||
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
||||
|
||||
;; Keep comment thread positions in sync on undo/redo
|
||||
(rx/of (dwcm/watch-comment-thread-position-changes stoper-s))
|
||||
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
@ -775,44 +775,46 @@
|
||||
#{:up :down :bottom :top})
|
||||
|
||||
(defn vertical-order-selected
|
||||
[loc]
|
||||
(dm/assert!
|
||||
"expected valid location"
|
||||
(contains? valid-vertical-locations loc))
|
||||
(ptk/reify ::vertical-order-selected
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
selected-ids (dsh/lookup-selected state)
|
||||
selected-shapes (map (d/getf objects) selected-ids)
|
||||
undo-id (js/Symbol)
|
||||
([loc]
|
||||
(vertical-order-selected loc nil))
|
||||
([loc ids]
|
||||
(dm/assert!
|
||||
"expected valid location"
|
||||
(contains? valid-vertical-locations loc))
|
||||
(ptk/reify ::vertical-order-selected
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
selected-ids (or ids (dsh/lookup-selected state))
|
||||
selected-shapes (map (d/getf objects) selected-ids)
|
||||
undo-id (js/Symbol)
|
||||
|
||||
move-shape
|
||||
(fn [changes shape]
|
||||
(let [parent (get objects (:parent-id shape))
|
||||
sibling-ids (:shapes parent)
|
||||
current-index (d/index-of sibling-ids (:id shape))
|
||||
index-in-selection (d/index-of selected-ids (:id shape))
|
||||
new-index (case loc
|
||||
:top (count sibling-ids)
|
||||
:down (max 0 (- current-index 1))
|
||||
:up (min (count sibling-ids) (+ (inc current-index) 1))
|
||||
:bottom index-in-selection)]
|
||||
(pcb/change-parent changes
|
||||
(:id parent)
|
||||
[shape]
|
||||
new-index)))
|
||||
move-shape
|
||||
(fn [changes shape]
|
||||
(let [parent (get objects (:parent-id shape))
|
||||
sibling-ids (:shapes parent)
|
||||
current-index (d/index-of sibling-ids (:id shape))
|
||||
index-in-selection (d/index-of selected-ids (:id shape))
|
||||
new-index (case loc
|
||||
:top (count sibling-ids)
|
||||
:down (max 0 (- current-index 1))
|
||||
:up (min (count sibling-ids) (+ (inc current-index) 1))
|
||||
:bottom index-in-selection)]
|
||||
(pcb/change-parent changes
|
||||
(:id parent)
|
||||
[shape]
|
||||
new-index)))
|
||||
|
||||
changes (reduce move-shape
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects))
|
||||
selected-shapes)]
|
||||
changes (reduce move-shape
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects))
|
||||
selected-shapes)]
|
||||
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update {:ids selected-ids})
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update {:ids selected-ids})
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn set-shape-index
|
||||
[file-id page-id id new-index]
|
||||
|
||||
@ -8,10 +8,13 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.main.data.changes :as dwc]
|
||||
[app.main.data.comments :as dcmt]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
@ -126,6 +129,14 @@
|
||||
ny (- (:y position) nh)]
|
||||
(update local :vbox assoc :x nx :y ny)))))))
|
||||
|
||||
(defn- set-comment-thread
|
||||
"Stores the comment thread in the workspace state so its bubble re-renders."
|
||||
[thread]
|
||||
(ptk/reify ::set-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:comment-threads (:id thread)] thread))))
|
||||
|
||||
(defn update-comment-thread-position
|
||||
([thread [new-x new-y]]
|
||||
(update-comment-thread-position thread [new-x new-y] nil))
|
||||
@ -136,35 +147,106 @@
|
||||
(dcmt/check-comment-thread! thread))
|
||||
(ptk/reify ::update-comment-thread-position
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(watch [it state _]
|
||||
(let [page (dsh/lookup-page state)
|
||||
page-id (:id page)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
frame-id (if (nil? frame-id)
|
||||
(ctst/get-frame-id-by-position objects (gpt/point new-x new-y))
|
||||
(:frame-id thread))
|
||||
thread (-> thread
|
||||
(assoc :position (gpt/point new-x new-y))
|
||||
(assoc :frame-id frame-id))
|
||||
thread-id (:id thread)]
|
||||
position (gpt/point new-x new-y)
|
||||
thread (-> thread
|
||||
(assoc :position position)
|
||||
(assoc :frame-id frame-id))
|
||||
thread-id (:id thread)
|
||||
|
||||
;; Record the position as a change so it joins the undo entry
|
||||
set-position-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position thread))]
|
||||
|
||||
(rx/concat
|
||||
(rx/of (fn [state]
|
||||
(-> state
|
||||
(update :comment-threads assoc thread-id thread)
|
||||
;; Keep the page positions map in sync so subsequent
|
||||
;; frame moves compute the relative offset from the
|
||||
;; latest position instead of a stale one.
|
||||
(dsh/update-page page-id
|
||||
#(update-in % [:comment-thread-positions thread-id]
|
||||
(fn [pos]
|
||||
(-> pos
|
||||
(assoc :position (:position thread))
|
||||
(assoc :frame-id (:frame-id thread)))))))))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
;; Update the new position in the rendered thread, and commit the
|
||||
;; change so the move is part of the undo entry
|
||||
(rx/of (set-comment-thread thread)
|
||||
(dwc/commit-changes set-position-changes))
|
||||
(->> (rp/cmd! :update-comment-thread-position {:id thread-id
|
||||
:position position
|
||||
:frame-id frame-id})
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore))))))))
|
||||
|
||||
(def ^:private undo-origins
|
||||
#{:app.main.data.workspace.undo/undo
|
||||
:app.main.data.workspace.undo/redo
|
||||
:app.main.data.workspace.undo/undo-to-index})
|
||||
|
||||
(defn- sync-comment-thread-position
|
||||
"Syncs the rendered thread and the backend for a comment position change."
|
||||
[{:keys [comment-thread-id position frame-id]}]
|
||||
(ptk/reify ::sync-comment-thread-position
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(cond-> state
|
||||
(and position frame-id)
|
||||
(update-in [:comment-threads comment-thread-id]
|
||||
(fn [thread]
|
||||
(some-> thread (assoc :position position :frame-id frame-id))))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(if (and position frame-id)
|
||||
(->> (rp/cmd! :update-comment-thread-position {:id comment-thread-id
|
||||
:position position
|
||||
:frame-id frame-id})
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore))
|
||||
(rx/empty)))))
|
||||
|
||||
(defn watch-comment-thread-position-changes
|
||||
"Syncs rendered threads and the backend when an undo/redo changes a comment position."
|
||||
[stopper]
|
||||
(ptk/reify ::watch-comment-thread-position-changes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(->> stream
|
||||
(rx/filter dwc/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(contains? undo-origins (:origin %)))
|
||||
(rx/mapcat (fn [commit]
|
||||
(->> (:redo-changes commit)
|
||||
(filter #(= :set-comment-thread-position (:type %)))
|
||||
(rx/from))))
|
||||
(rx/map sync-comment-thread-position)
|
||||
(rx/take-until stopper)))))
|
||||
|
||||
(defn frame-pin-transform
|
||||
"Matrix that moves a comment pinned inside `frame` as the frame is transformed,
|
||||
following its translation and rotation but not its resize scale."
|
||||
[frame modifiers transform]
|
||||
(when (and (some? frame) (or (some? modifiers) (some? transform)))
|
||||
(let [frame' (cond-> frame
|
||||
(some? modifiers) (gsh/transform-shape modifiers)
|
||||
(some? transform) (gsh/apply-transform transform))
|
||||
|
||||
c (gsh/shape->center frame)
|
||||
c' (gsh/shape->center frame')
|
||||
tfi (or (:transform-inverse frame) (gmt/matrix))
|
||||
tf' (or (:transform frame') (gmt/matrix))
|
||||
sr (:selrect frame)
|
||||
sr' (:selrect frame')
|
||||
d (gpt/point (- (:x sr') (:x sr))
|
||||
(- (:y sr') (:y sr)))]
|
||||
(-> (gmt/matrix)
|
||||
(gmt/translate! c')
|
||||
(gmt/multiply! tf')
|
||||
(gmt/translate! (gpt/negate c'))
|
||||
(gmt/translate! d)
|
||||
(gmt/translate! c)
|
||||
(gmt/multiply! tfi)
|
||||
(gmt/translate! (gpt/negate c))))))
|
||||
|
||||
;; Move comment threads that are inside a frame when that frame is moved"
|
||||
|
||||
(defn- move-frame-comment-threads
|
||||
@ -187,25 +269,18 @@
|
||||
|
||||
build-move-event
|
||||
(fn [comment-thread]
|
||||
(let [frame-id (:frame-id comment-thread)
|
||||
(let [frame-id (:frame-id comment-thread)
|
||||
frame (get objects frame-id)
|
||||
modifiers (get-in object-modifiers [frame-id :modifiers])
|
||||
transform (get transforms frame-id)
|
||||
|
||||
frame'
|
||||
(cond-> frame
|
||||
(some? modifiers)
|
||||
(gsh/transform-shape modifiers)
|
||||
matrix (frame-pin-transform frame modifiers transform)
|
||||
|
||||
(some? transform)
|
||||
(gsh/apply-transform transform))
|
||||
|
||||
moved (gpt/to-vec (gpt/point (:x frame) (:y frame))
|
||||
(gpt/point (:x frame') (:y frame')))
|
||||
position (get-in threads-position-map [(:id comment-thread) :position])
|
||||
new-x (+ (:x position) (:x moved))
|
||||
new-y (+ (:y position) (:y moved))]
|
||||
(update-comment-thread-position comment-thread [new-x new-y] (:id frame))))]
|
||||
position' (cond-> position
|
||||
(some? matrix)
|
||||
(gpt/transform matrix))]
|
||||
(update-comment-thread-position comment-thread [(:x position') (:y position')] frame-id)))]
|
||||
|
||||
(->> (:comment-threads state)
|
||||
(vals)
|
||||
|
||||
@ -1380,7 +1380,7 @@
|
||||
[]
|
||||
(ptk/reify ::watch-component-changes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(watch [_ state stream]
|
||||
(let [stopper-s
|
||||
(->> stream
|
||||
(rx/map ptk/type)
|
||||
@ -1450,7 +1450,7 @@
|
||||
(rx/tap #(log/trc :hint "buffer initialized")))]
|
||||
|
||||
(when (or (contains? cf/flags :component-thumbnails)
|
||||
(features/active-feature? @st/state "render-wasm/v1"))
|
||||
(features/active-feature? state "render-wasm/v1"))
|
||||
(->> (rx/merge
|
||||
changes-s
|
||||
|
||||
@ -1458,7 +1458,7 @@
|
||||
;; change so single edits (fill, etc.) update instantly.
|
||||
;; Non-WASM persists on every render, so it stays on the
|
||||
;; debounced path below to avoid per-edit backend posts.
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(->> changes-s
|
||||
(rx/filter (ptk/type? ::component-changed))
|
||||
(rx/map deref)
|
||||
|
||||
@ -343,20 +343,23 @@
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-token-set
|
||||
[id]
|
||||
(ptk/reify ::duplicate-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")]
|
||||
([id]
|
||||
(duplicate-token-set id nil))
|
||||
([id {:keys [id-ref]}]
|
||||
(ptk/reify ::duplicate-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")]
|
||||
|
||||
(when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
(when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})]
|
||||
(when id-ref (reset! id-ref (ctob/get-id token-set)))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes)))))))))
|
||||
|
||||
(defn set-enabled-token-set
|
||||
[name enabled?]
|
||||
|
||||
@ -327,12 +327,21 @@
|
||||
(dwm/create-modif-tree shape-ids %)
|
||||
:ignore-constraints (contains? layout :scale-text)))))
|
||||
|
||||
(->> resize-events-stream
|
||||
(rx/mapcat
|
||||
(let [emit-modifiers
|
||||
(fn [modifiers]
|
||||
(let [modif-tree (dwm/create-modif-tree shape-ids modifiers)]
|
||||
(rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text))))))
|
||||
(rx/take-until stopper)))]
|
||||
(rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text)))))]
|
||||
;; Throttle the live preview to limit re-renders; the trailing
|
||||
;; rx/last applies the exact final frame.
|
||||
(rx/merge
|
||||
(->> resize-events-stream
|
||||
(rx/sample 16)
|
||||
(rx/mapcat emit-modifiers)
|
||||
(rx/take-until stopper))
|
||||
(->> resize-events-stream
|
||||
(rx/take-until stopper)
|
||||
(rx/last)
|
||||
(rx/mapcat emit-modifiers)))))]
|
||||
|
||||
(rx/concat
|
||||
;; This initial stream waits for some pixels to be move before making the resize
|
||||
@ -523,14 +532,22 @@
|
||||
|
||||
(rx/of (finish-transform)))
|
||||
|
||||
(rx/concat
|
||||
(rx/merge
|
||||
(->> angle-stream
|
||||
(rx/map
|
||||
#(dwm/set-rotation-modifiers % shapes group-center))
|
||||
(rx/take-until stopper)))
|
||||
(rx/of (dwm/apply-modifiers)
|
||||
(finish-transform))))))))
|
||||
(let [emit-modifiers
|
||||
(fn [angle] (dwm/set-rotation-modifiers angle shapes group-center))]
|
||||
;; Throttle the live preview to limit re-renders; the trailing
|
||||
;; rx/last applies the exact final frame.
|
||||
(rx/concat
|
||||
(rx/merge
|
||||
(->> angle-stream
|
||||
(rx/sample 16)
|
||||
(rx/map emit-modifiers)
|
||||
(rx/take-until stopper))
|
||||
(->> angle-stream
|
||||
(rx/take-until stopper)
|
||||
(rx/last)
|
||||
(rx/map emit-modifiers)))
|
||||
(rx/of (dwm/apply-modifiers)
|
||||
(finish-transform)))))))))
|
||||
|
||||
(defn increase-rotation
|
||||
"Rotate shapes a fixed angle, from a keyboard action."
|
||||
@ -822,6 +839,8 @@
|
||||
|
||||
(rx/merge
|
||||
(->> modifiers-stream
|
||||
;; Throttle the live preview to limit re-renders.
|
||||
(rx/sample 16)
|
||||
(rx/map
|
||||
(fn [[modifiers snap-ignore-axis]]
|
||||
(dwm/set-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis}))))
|
||||
@ -843,10 +862,13 @@
|
||||
;; Last event will write the modifiers creating the changes
|
||||
(->> move-stream
|
||||
(rx/last)
|
||||
(rx/with-latest-from modifiers-stream)
|
||||
(rx/mapcat
|
||||
(fn [[_ target-frame drop-index drop-cell]]
|
||||
(fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
;; Apply the exact final modifiers; the preview may drop the last frame.
|
||||
(dwm/set-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis})
|
||||
(dwm/apply-modifiers {:undo-transation? false})
|
||||
(move-shapes-to-frame ids target-frame drop-index drop-cell)
|
||||
(finish-transform)
|
||||
|
||||
@ -1164,6 +1164,9 @@
|
||||
|
||||
test-id (str/join "-" (map :seqn (sort-by :seqn thread-group)))
|
||||
|
||||
;; Click-through while transforming a shape, so it doesn't capture the drag
|
||||
dragging? (some? (mf/deref refs/current-transform))
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps thread-group position zoom)
|
||||
@ -1176,7 +1179,8 @@
|
||||
(dwz/set-zoom position scale-zoom)))))]
|
||||
|
||||
[:div {:style {:top (dm/str pos-y "px")
|
||||
:left (dm/str pos-x "px")}
|
||||
:left (dm/str pos-x "px")
|
||||
:pointer-events (when dragging? "none")}
|
||||
:on-click on-click
|
||||
:class (stl/css :floating-preview-wrapper :floating-preview-bubble)}
|
||||
[:> comment-avatar*
|
||||
@ -1198,6 +1202,9 @@
|
||||
|
||||
frame-id (:frame-id thread)
|
||||
|
||||
;; Click-through while transforming a shape, so it doesn't capture the drag
|
||||
dragging? (some? (mf/deref refs/current-transform))
|
||||
|
||||
state (mf/use-state
|
||||
#(do {:is-hover false
|
||||
:is-grabbing false
|
||||
@ -1290,7 +1297,8 @@
|
||||
(on-click thread))))]
|
||||
|
||||
[:div {:style {:top (dm/str pos-y "px")
|
||||
:left (dm/str pos-x "px")}
|
||||
:left (dm/str pos-x "px")
|
||||
:pointer-events (when dragging? "none")}
|
||||
:on-pointer-down on-pointer-down
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-pointer-move on-pointer-move
|
||||
|
||||
@ -459,7 +459,7 @@
|
||||
(into [] (distinct) (conj coll item)))
|
||||
|
||||
(mf/defc multi-input
|
||||
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}]
|
||||
[{:keys [form label class trim valid-item-fn caution-item-fn on-submit] :as props}]
|
||||
(let [form (or form (mf/use-ctx form-ctx))
|
||||
input-name (get props :name)
|
||||
touched? (get-in @form [:touched input-name])
|
||||
@ -610,6 +610,7 @@
|
||||
|
||||
[:div {:class klass}
|
||||
[:input {:id (name input-name)
|
||||
:name (name input-name)
|
||||
:class in-klass
|
||||
:type "text"
|
||||
:auto-focus auto-focus?
|
||||
|
||||
@ -168,10 +168,13 @@
|
||||
::mf/register-as :invite-members
|
||||
::mf/props :obj}
|
||||
[{:keys [team origin invite-email]}]
|
||||
(let [members (get team :members)
|
||||
(let [teams (mf/deref refs/teams)
|
||||
|
||||
perms (get team :permissions)
|
||||
team-id (get team :id)
|
||||
|
||||
members (get-in teams [team-id :members])
|
||||
|
||||
roles (mf/with-memo [perms]
|
||||
(get-available-roles perms))
|
||||
|
||||
@ -824,6 +827,7 @@
|
||||
[:div {:class (stl/css :empty-invitations-buttons)}
|
||||
[:a
|
||||
{:class (stl/css :btn-empty-invitations)
|
||||
:role "button"
|
||||
:on-click on-invite-member
|
||||
:data-testid "invite-member"}
|
||||
(tr "dashboard.invite-profile")]]
|
||||
|
||||
@ -959,32 +959,17 @@
|
||||
ev-name (if (= next-renderer :wasm)
|
||||
"enable-webgl-rendering"
|
||||
"disable-webgl-rendering")]
|
||||
|
||||
(if (cf/external-feature-flag "renderer-hard-reload" "test")
|
||||
;; Bare RPC + hard reload: skips `du/update-profile-props`, so
|
||||
;; `features/recompute-features` is not run here; bootstrap
|
||||
;; after reload resolves render-wasm/v1 from the saved profile.
|
||||
(do
|
||||
(->> (rx/zip
|
||||
(rp/cmd! :update-profile-props {:props {:renderer next-renderer}})
|
||||
(rx/filter (ptk/type? ::ev/chunk-persisted) st/stream))
|
||||
(rx/timeout 2000 (rx/of :timeout))
|
||||
(rx/subs! (fn [_]
|
||||
(dom/reload-current-window true))
|
||||
(fn [_]
|
||||
(st/emit! (ntf/error (tr "errors.generic"))))))
|
||||
(st/emit! (ev/event {::ev/name ev-name
|
||||
::ev/origin "workspace:menu"})
|
||||
(ptk/data-event ::ev/force-persist {})))
|
||||
|
||||
;; `update-profile-props` WatchEvent calls
|
||||
;; `features/recompute-features`.
|
||||
(st/emit! (ev/event {::ev/name ev-name
|
||||
::ev/origin "workspace:menu"})
|
||||
(du/update-profile-props {:renderer next-renderer})
|
||||
(ntf/success (tr (if (= next-renderer :wasm)
|
||||
"webgl.toast.webgl-render-enabled"
|
||||
"webgl.toast.webgl-render-disabled"))))))))
|
||||
(->> (rx/zip
|
||||
(rp/cmd! :update-profile-props {:props {:renderer next-renderer}})
|
||||
(rx/filter (ptk/type? ::ev/chunk-persisted) st/stream))
|
||||
(rx/timeout 2000 (rx/of :timeout))
|
||||
(rx/subs! (fn [_]
|
||||
(dom/reload-current-window true))
|
||||
(fn [_]
|
||||
(st/emit! (ntf/error (tr "errors.generic"))))))
|
||||
(st/emit! (ev/event {::ev/name ev-name
|
||||
::ev/origin "workspace:menu"})
|
||||
(ptk/data-event ::ev/force-persist {})))))
|
||||
|
||||
open-plugins-manager
|
||||
(mf/use-fn
|
||||
|
||||
@ -48,7 +48,12 @@
|
||||
:selrect
|
||||
:points
|
||||
:show-content
|
||||
:hide-in-viewer])
|
||||
:hide-in-viewer
|
||||
|
||||
;; Needed to disable/enable width/height
|
||||
;; otherwise the memo will not detect changes
|
||||
:layout-item-h-sizing
|
||||
:layout-item-v-sizing])
|
||||
|
||||
(def ^:private generic-options
|
||||
#{:size :position :rotation})
|
||||
@ -130,7 +135,7 @@
|
||||
acc))))
|
||||
acc)))))
|
||||
|
||||
(defn- check-measures-menu-props
|
||||
(defn check-measures-menu-props
|
||||
[old-props new-props]
|
||||
(let [o-values (unchecked-get old-props "values")
|
||||
n-values (unchecked-get new-props "values")]
|
||||
@ -150,10 +155,12 @@
|
||||
(get n-values :hide-in-viewer))
|
||||
(identical? (get o-values :width)
|
||||
(get n-values :width))
|
||||
(identical? (get o-values :width)
|
||||
(get n-values :width))
|
||||
(identical? (get o-values :height)
|
||||
(get n-values :height))
|
||||
(identical? (get o-values :layout-item-h-sizing)
|
||||
(get n-values :layout-item-h-sizing))
|
||||
(identical? (get o-values :layout-item-v-sizing)
|
||||
(get n-values :layout-item-v-sizing))
|
||||
(identical? (get o-values :points)
|
||||
(get n-values :points))
|
||||
(identical? (get o-values :selrect)
|
||||
|
||||
@ -8,9 +8,6 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.refs :as refs]
|
||||
@ -18,6 +15,47 @@
|
||||
[app.main.ui.comments :as cmt]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Pin transform for the bubble's frame so it follows the frame during a drag,
|
||||
;; scoped per frame to avoid re-rendering the whole layer each tick.
|
||||
(defn- use-frame-position-modifier
|
||||
[frame-id]
|
||||
(let [modifiers (mf/deref refs/workspace-modifiers)
|
||||
wasm-mods (mf/deref refs/workspace-wasm-modifiers)
|
||||
objects (mf/deref refs/workspace-page-objects)]
|
||||
(dwcm/frame-pin-transform (get objects frame-id)
|
||||
(get-in modifiers [frame-id :modifiers])
|
||||
(get wasm-mods frame-id))))
|
||||
|
||||
(mf/defc comment-floating-bubble-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [thread zoom is-open]}]
|
||||
(let [position-modifier (use-frame-position-modifier (:frame-id thread))]
|
||||
[:> cmt/comment-floating-bubble*
|
||||
{:thread thread
|
||||
:zoom zoom
|
||||
:position-modifier position-modifier
|
||||
:is-open is-open}]))
|
||||
|
||||
(mf/defc comment-floating-group-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [thread-group zoom]}]
|
||||
(let [thread (first thread-group)
|
||||
position-modifier (use-frame-position-modifier (:frame-id thread))]
|
||||
[:> cmt/comment-floating-group*
|
||||
{:thread-group thread-group
|
||||
:zoom zoom
|
||||
:position-modifier position-modifier}]))
|
||||
|
||||
(mf/defc comment-floating-thread-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [thread viewport zoom]}]
|
||||
(let [position-modifier (use-frame-position-modifier (:frame-id thread))]
|
||||
[:> cmt/comment-floating-thread*
|
||||
{:thread thread
|
||||
:viewport viewport
|
||||
:position-modifier position-modifier
|
||||
:zoom zoom}]))
|
||||
|
||||
(mf/defc comments-layer*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [vbox vport zoom file-id page-id]}]
|
||||
@ -34,38 +72,12 @@
|
||||
|
||||
threads-map (mf/deref refs/threads)
|
||||
|
||||
;; Active transform modifiers (e.g. while dragging a board). We use
|
||||
;; them to move comment bubbles live alongside their frame, instead of
|
||||
;; only repositioning them at drop time. The SVG (legacy) renderer keeps
|
||||
;; them in `:workspace-modifiers`, while the WASM renderer pushes them
|
||||
;; through the `wasm-modifiers` stream as plain transform matrices.
|
||||
modifiers (mf/deref refs/workspace-modifiers)
|
||||
wasm-mods (into {} (mf/deref refs/workspace-wasm-modifiers))
|
||||
objects (mf/deref refs/workspace-page-objects)
|
||||
|
||||
threads
|
||||
(mf/with-memo [threads-map local profile page-id]
|
||||
(->> (vals threads-map)
|
||||
(filter #(= (:page-id %) page-id))
|
||||
(dcm/apply-filters local profile)))
|
||||
|
||||
;; Returns the position translation matrix for a frame that is being
|
||||
;; transformed, or nil when the frame has no active modifier. The delta
|
||||
;; matches `move-frame-comment-threads` (frame top-left displacement) so
|
||||
;; the bubble does not jump when the modifier is committed.
|
||||
frame-position-modifier
|
||||
(fn [frame-id]
|
||||
(when-let [frame (get objects frame-id)]
|
||||
(let [frame'
|
||||
(if-let [modifier (get-in modifiers [frame-id :modifiers])]
|
||||
(gsh/transform-shape frame modifier)
|
||||
(when-let [transform (get wasm-mods frame-id)]
|
||||
(gsh/apply-transform frame transform)))]
|
||||
(when (some? frame')
|
||||
(let [delta (gpt/to-vec (gpt/point (:x frame) (:y frame))
|
||||
(gpt/point (:x frame') (:y frame')))]
|
||||
(gmt/translate-matrix delta))))))
|
||||
|
||||
viewport
|
||||
(assoc vport :offset-x pos-x :offset-y pos-y)
|
||||
|
||||
@ -94,23 +106,20 @@
|
||||
(let [group? (> (count thread-group) 1)
|
||||
thread (first thread-group)]
|
||||
(if group?
|
||||
[:> cmt/comment-floating-group* {:thread-group thread-group
|
||||
:zoom zoom
|
||||
:position-modifier (frame-position-modifier (:frame-id thread))
|
||||
:key (:seqn thread)}]
|
||||
[:> cmt/comment-floating-bubble* {:thread thread
|
||||
:zoom zoom
|
||||
:position-modifier (frame-position-modifier (:frame-id thread))
|
||||
:is-open (= (:id thread) (:open local))
|
||||
:key (:seqn thread)}])))
|
||||
[:> comment-floating-group-wrapper* {:thread-group thread-group
|
||||
:zoom zoom
|
||||
:key (:seqn thread)}]
|
||||
[:> comment-floating-bubble-wrapper* {:thread thread
|
||||
:zoom zoom
|
||||
:is-open (= (:id thread) (:open local))
|
||||
:key (:seqn thread)}])))
|
||||
|
||||
(when-let [id (:open local)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
(when (seq (dcm/apply-filters local profile [thread]))
|
||||
[:> cmt/comment-floating-thread*
|
||||
[:> comment-floating-thread-wrapper*
|
||||
{:thread thread
|
||||
:viewport viewport
|
||||
:position-modifier (frame-position-modifier (:frame-id thread))
|
||||
:zoom zoom}])))
|
||||
|
||||
(when-let [draft (:draft local)]
|
||||
|
||||
@ -317,6 +317,11 @@
|
||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||
(u/not-valid plugin-id :group-shapes shapes)
|
||||
|
||||
;; A group cannot be created from no shapes; per the documented contract
|
||||
;; return null instead of a proxy pointing at a shape that never exists.
|
||||
(zero? (alength shapes))
|
||||
nil
|
||||
|
||||
(some #(not (u/page-active? (obj/get % "$page"))) shapes)
|
||||
(u/not-valid plugin-id :group "Cannot modify a page that is not currently active")
|
||||
|
||||
@ -664,8 +669,13 @@
|
||||
(u/not-valid plugin-id :flatten-shapes "Not valid shapes")
|
||||
|
||||
:else
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
(st/emit! (dw/convert-selected-to-path ids)))))
|
||||
;; convert-selected-to-path converts the shapes in place (keeping their
|
||||
;; ids), so return proxies for the same ids, now resolving as paths.
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
page-id (:current-page-id @st/state)
|
||||
ids (mapv #(obj/get % "$id") shapes)]
|
||||
(st/emit! (dw/convert-selected-to-path (into #{} ids)))
|
||||
(apply array (map #(shape/shape-proxy plugin-id file-id page-id %) ids)))))
|
||||
|
||||
:createVariantFromComponents
|
||||
(fn [shapes]
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.comments :as dc]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.comments :as dwc]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
@ -203,13 +202,12 @@
|
||||
|
||||
:remove
|
||||
(fn []
|
||||
(let [profile (:profile @st/state)
|
||||
owner (dsh/lookup-profile @st/state (:owner-id data))]
|
||||
(let [profile (:profile @st/state)]
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "comment:write"))
|
||||
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
|
||||
|
||||
(not= (:id profile) owner)
|
||||
(not= (:id profile) (:owner-id data))
|
||||
(u/not-valid plugin-id :remove "Cannot change content from another user's comments")
|
||||
|
||||
:else
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
(user/user-proxy plugin-id user-data)))}
|
||||
|
||||
:createdAt
|
||||
{:get #(.toJSDate ^js (:created-at @data))}
|
||||
{:get #(:created-at @data)}
|
||||
|
||||
:isAutosave
|
||||
{:get #(= "system" (:created-by @data))}
|
||||
@ -136,6 +136,9 @@
|
||||
:name
|
||||
{:get #(-> (u/locate-file id) :name)}
|
||||
|
||||
:revn
|
||||
{:get #(-> (u/locate-file id) :revn)}
|
||||
|
||||
:pages
|
||||
{:this true
|
||||
:get #(.getPages ^js %)}
|
||||
|
||||
@ -6,17 +6,24 @@
|
||||
|
||||
(ns app.plugins.flags
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn clear
|
||||
[id]
|
||||
(ptk/reify ::reset
|
||||
(defn initialize
|
||||
"Initialize flags values for plugins"
|
||||
[id version]
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:plugins :flags] assoc id {}))))
|
||||
(let [version (d/nilv version 1)]
|
||||
(update-in state [:plugins :flags] assoc id
|
||||
{:natural-child-ordering false
|
||||
;; For version >= 2 harden the contract by throwing errors
|
||||
;; on validation failures
|
||||
:throw-validation-errors (>= version 2)})))))
|
||||
|
||||
(defn- set-flag
|
||||
[id key value]
|
||||
|
||||
@ -64,13 +64,13 @@
|
||||
(u/not-valid plugin-id :applyToText "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(let [id (obj/get text "$id")
|
||||
(let [text-id (obj/get text "$id")
|
||||
values {:font-id id
|
||||
:font-family family
|
||||
:font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant))
|
||||
:font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant))
|
||||
:font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}]
|
||||
(st/emit! (dwt/update-attrs id values)))))
|
||||
(st/emit! (dwt/update-attrs text-id values)))))
|
||||
|
||||
:applyToRange
|
||||
(fn [range variant]
|
||||
@ -85,15 +85,15 @@
|
||||
(u/not-valid plugin-id :applyToRange "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(let [id (obj/get range "$id")
|
||||
start (obj/get range "start")
|
||||
end (obj/get range "end")
|
||||
(let [range-id (obj/get range "$id")
|
||||
start (obj/get range "$start")
|
||||
end (obj/get range "$end")
|
||||
values {:font-id id
|
||||
:font-family family
|
||||
:font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant))
|
||||
:font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant))
|
||||
:font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}]
|
||||
(st/emit! (dwt/update-text-range id start end values)))))))))
|
||||
(st/emit! (dwt/update-text-range range-id start end values)))))))))
|
||||
|
||||
(defn fonts-subcontext
|
||||
[plugin-id]
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
:frame "board"
|
||||
:rect "rectangle"
|
||||
:circle "ellipse"
|
||||
:bool "boolean"
|
||||
(d/name type)))
|
||||
|
||||
;;export type Bounds = {
|
||||
@ -146,7 +147,7 @@
|
||||
[[color attrs]]
|
||||
(let [shapes-info (apply array (map format-shape-info attrs))
|
||||
color (format-color color)]
|
||||
(obj/set! color "shapeInfo" shapes-info)
|
||||
(obj/set! color "shapesInfo" shapes-info)
|
||||
color))
|
||||
|
||||
|
||||
|
||||
@ -301,11 +301,15 @@
|
||||
|
||||
:addRowAtIndex
|
||||
(fn [index type value]
|
||||
(let [type (keyword type)]
|
||||
(let [type (keyword type)
|
||||
num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :addRowAtIndex-index index)
|
||||
|
||||
(or (< index 0) (> index num-rows))
|
||||
(u/not-valid plugin-id :addRowAtIndex-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :addRowAtIndex-type type)
|
||||
|
||||
@ -344,64 +348,80 @@
|
||||
|
||||
:addColumnAtIndex
|
||||
(fn [index type value]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||
(let [type (keyword type)
|
||||
num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-type type)
|
||||
(or (< index 0) (> index num-columns))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(not (sm/valid-safe-number? value)))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-value value)
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-type type)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(not (sm/valid-safe-number? value)))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-value value)
|
||||
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(let [type (keyword type)]
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value} index)))))
|
||||
|
||||
:removeRow
|
||||
(fn [index]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeRow index)
|
||||
(let [num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeRow index)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
|
||||
(or (< index 0) (>= index num-rows))
|
||||
(u/not-valid plugin-id :removeRow index)
|
||||
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :row index))))
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :row index)))))
|
||||
|
||||
:removeColumn
|
||||
(fn [index]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeColumn index)
|
||||
(let [num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeColumn index)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
|
||||
(or (< index 0) (>= index num-columns))
|
||||
(u/not-valid plugin-id :removeColumn index)
|
||||
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :column index))))
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :column index)))))
|
||||
|
||||
:setColumn
|
||||
(fn [index type value]
|
||||
(let [type (keyword type)]
|
||||
(let [type (keyword type)
|
||||
num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :setColumn-index index)
|
||||
|
||||
(or (< index 0) (>= index num-columns))
|
||||
(u/not-valid plugin-id :setColumn-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :setColumn-type type)
|
||||
|
||||
@ -420,11 +440,15 @@
|
||||
|
||||
:setRow
|
||||
(fn [index type value]
|
||||
(let [type (keyword type)]
|
||||
(let [type (keyword type)
|
||||
num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :setRow-index index)
|
||||
|
||||
(or (< index 0) (>= index num-rows))
|
||||
(u/not-valid plugin-id :setRow-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :setRow-type type)
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
|
||||
:id {:get (fn [] (dm/str id))}
|
||||
:libraryId {:get (fn [] (dm/str file-id))}
|
||||
:fileId {:get #(dm/str file-id)}
|
||||
|
||||
:name
|
||||
@ -101,7 +102,8 @@
|
||||
|
||||
:else
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
(assoc :color value))]
|
||||
(assoc :color value)
|
||||
(dissoc :gradient :image))]
|
||||
(st/emit! (dwl/update-color-data color file-id)))))}
|
||||
|
||||
:opacity
|
||||
@ -136,7 +138,8 @@
|
||||
|
||||
:else
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
(assoc :gradient value))]
|
||||
(assoc :gradient value)
|
||||
(dissoc :color :image))]
|
||||
(st/emit! (dwl/update-color-data color file-id))))))}
|
||||
|
||||
:image
|
||||
@ -154,7 +157,8 @@
|
||||
|
||||
:else
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
(assoc :image value))]
|
||||
(assoc :image value)
|
||||
(dissoc :color :gradient))]
|
||||
(st/emit! (dwl/update-color-data color file-id))))))}
|
||||
|
||||
:remove
|
||||
@ -295,6 +299,7 @@
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
:id {:get (fn [] (dm/str id))}
|
||||
:libraryId {:get (fn [] (dm/str file-id))}
|
||||
|
||||
:name
|
||||
{:this true
|
||||
@ -484,6 +489,27 @@
|
||||
(assoc :text-transform value))]
|
||||
(st/emit! (dwl/update-typography typo file-id)))))}
|
||||
|
||||
:setFont
|
||||
(fn [font variant]
|
||||
(cond
|
||||
(not (obj/type-of? font "FontProxy"))
|
||||
(u/not-valid plugin-id :setFont font)
|
||||
|
||||
(not (r/check-permission plugin-id "library:write"))
|
||||
(u/not-valid plugin-id :setFont "Plugin doesn't have 'library:write' permission")
|
||||
|
||||
:else
|
||||
;; When a variant is given read the variant-specific fields from it;
|
||||
;; otherwise the FontProxy exposes the font's default variant fields.
|
||||
(let [source (if (obj/type-of? variant "FontVariantProxy") variant font)
|
||||
typo (-> (u/locate-library-typography file-id id)
|
||||
(assoc :font-id (obj/get font "fontId")
|
||||
:font-family (obj/get font "fontFamily")
|
||||
:font-variant-id (obj/get source "fontVariantId")
|
||||
:font-style (obj/get source "fontStyle")
|
||||
:font-weight (obj/get source "fontWeight")))]
|
||||
(st/emit! (dwl/update-typography typo file-id)))))
|
||||
|
||||
:remove
|
||||
(fn []
|
||||
(cond
|
||||
@ -539,8 +565,8 @@
|
||||
|
||||
:else
|
||||
(let [shape-id (obj/get range "$id")
|
||||
start (obj/get range "start")
|
||||
end (obj/get range "end")
|
||||
start (obj/get range "$start")
|
||||
end (obj/get range "$end")
|
||||
typography (u/locate-library-typography file-id id)
|
||||
attrs (-> typography
|
||||
(assoc :typography-ref-file file-id)
|
||||
@ -718,6 +744,7 @@
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
:id {:get (fn [] (dm/str id))}
|
||||
:libraryId {:get (fn [] (dm/str file-id))}
|
||||
|
||||
:name
|
||||
{:this true
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
(u/not-valid plugin-id :removeItem "The key must be a string")
|
||||
|
||||
:else
|
||||
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
||||
(.removeItem ^js local-storage (prefix-key plugin-id key))))
|
||||
|
||||
:getKeys
|
||||
(fn []
|
||||
|
||||
@ -412,8 +412,7 @@
|
||||
(js/Promise.
|
||||
(fn [resolve]
|
||||
(let [thread-id (obj/get thread "$id")]
|
||||
(js/Promise.
|
||||
(st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve)))))))))
|
||||
(st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve))))))))
|
||||
|
||||
:findCommentThreads
|
||||
(fn [criteria]
|
||||
|
||||
@ -343,10 +343,10 @@
|
||||
(when (some? guide)
|
||||
(case (obj/get guide "type")
|
||||
"column"
|
||||
parse-frame-guide-column
|
||||
(parse-frame-guide-column guide)
|
||||
|
||||
"row"
|
||||
parse-frame-guide-row
|
||||
(parse-frame-guide-row guide)
|
||||
|
||||
"square"
|
||||
(parse-frame-guide-square guide))))
|
||||
@ -489,7 +489,7 @@
|
||||
:destination (-> (obj/get action "destination") (obj/get "$id"))
|
||||
:relative-to (-> (obj/get action "relativeTo") (obj/get "$id"))
|
||||
:overlay-pos-type (-> (obj/get action "position") parse-keyword)
|
||||
:overlay-position (-> (obj/get action "manualPositionLocation") parse-point)
|
||||
:overlay-position (-> (obj/get action "manualPositionLocation") parse-point (d/nilv (gpt/point 0 0)))
|
||||
:close-click-outside (obj/get action "closeWhenClickOutside")
|
||||
:background-overlay (obj/get action "addBackgroundOverlay")
|
||||
:animation (-> (obj/get action "animation") parse-animation)}
|
||||
|
||||
@ -14,10 +14,15 @@
|
||||
[app.plugins.utils :as u]))
|
||||
|
||||
(defn ^:export centerShapes
|
||||
[plugin-id shapes]
|
||||
[shapes]
|
||||
(cond
|
||||
(not (every? shape/shape-proxy? shapes))
|
||||
(u/not-valid plugin-id :centerShapes shapes)
|
||||
(u/not-valid nil :centerShapes shapes)
|
||||
|
||||
;; The documented contract returns null for an empty array; without this
|
||||
;; guard `shapes->rect` yields a non-rect and `rect->center` asserts.
|
||||
(empty? shapes)
|
||||
nil
|
||||
|
||||
:else
|
||||
(let [shapes (->> shapes (map u/proxy->shape))]
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
{:id shape-id}
|
||||
(u/locate-shape file-id page-id shape-id)
|
||||
index
|
||||
#(assoc % :event-type value)
|
||||
{:page-id page-id})))))}
|
||||
@ -117,7 +117,7 @@
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
{:id shape-id}
|
||||
(u/locate-shape file-id page-id shape-id)
|
||||
index
|
||||
#(assoc % :delay value)
|
||||
{:page-id page-id}))))}
|
||||
@ -137,7 +137,7 @@
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
{:id shape-id}
|
||||
(u/locate-shape file-id page-id shape-id)
|
||||
index
|
||||
#(d/patch-object % params)
|
||||
{:page-id page-id})))))}
|
||||
@ -592,7 +592,7 @@
|
||||
:else
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
|
||||
|
||||
:background-blur
|
||||
:backgroundBlur
|
||||
{:this true
|
||||
:get #(-> % u/proxy->shape :background-blur format/format-blur)
|
||||
:set
|
||||
@ -1249,6 +1249,11 @@
|
||||
:else
|
||||
(st/emit! (dwg/unmask-group #{id})))))
|
||||
|
||||
:isMask
|
||||
(fn []
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(boolean (cfh/mask-shape? shape))))
|
||||
|
||||
;; Only for path and bool shapes
|
||||
:toD
|
||||
(fn []
|
||||
@ -1315,19 +1320,19 @@
|
||||
|
||||
:bringForward
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :up)))
|
||||
(st/emit! (dw/vertical-order-selected :up [id])))
|
||||
|
||||
:sendBackward
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :down)))
|
||||
(st/emit! (dw/vertical-order-selected :down [id])))
|
||||
|
||||
:bringToFront
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :top)))
|
||||
(st/emit! (dw/vertical-order-selected :top [id])))
|
||||
|
||||
:sendToBack
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :bottom)))
|
||||
(st/emit! (dw/vertical-order-selected :bottom [id])))
|
||||
|
||||
;; COMPONENTS
|
||||
:isComponentInstance
|
||||
@ -1402,6 +1407,28 @@
|
||||
:else
|
||||
(st/emit! (dwl/detach-component id))))
|
||||
|
||||
:swapComponent
|
||||
(fn [component]
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :swapComponent "Cannot modify a page that is not currently active")
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :swapComponent "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (obj/type-of? component "LibraryComponentProxy"))
|
||||
(u/not-valid plugin-id :swapComponent "Component not valid")
|
||||
|
||||
(not (ctk/in-component-copy? shape))
|
||||
(u/not-valid plugin-id :swapComponent "The shape is not a component copy instance")
|
||||
|
||||
:else
|
||||
(st/emit! (dwl/component-swap shape
|
||||
(obj/get component "$file")
|
||||
(obj/get component "$id")
|
||||
true)))))
|
||||
|
||||
;; Export
|
||||
:export
|
||||
(fn [value]
|
||||
@ -1536,7 +1563,7 @@
|
||||
(rg/ruler-guide-proxy plugin-id file-id page-id ruler-id)))))
|
||||
|
||||
:removeRulerGuide
|
||||
(fn [_ value]
|
||||
(fn [value]
|
||||
(cond
|
||||
(not (rg/ruler-guide-proxy? value))
|
||||
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
|
||||
@ -1618,7 +1645,7 @@
|
||||
|
||||
:else
|
||||
(let [ids
|
||||
(into #{id} (keep uuid/parse*) id)
|
||||
(into #{id} (keep uuid/parse*) ids)
|
||||
|
||||
valid?
|
||||
(every?
|
||||
@ -1740,7 +1767,7 @@
|
||||
(let [id (obj/get self "$id")
|
||||
value (parser/parse-frame-guides value)]
|
||||
(cond
|
||||
(not (sm/validate [:vector ::ctg/grid] value))
|
||||
(not (sm/validate [:vector ctg/schema:grid] value))
|
||||
(u/not-valid plugin-id :guides value)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
taking? (or taking? (and (<= from start) (< start to)))
|
||||
text (subs text (max 0 (- start acc)) (- end acc))
|
||||
result (cond-> result
|
||||
(and taking? (d/not-empty? text))
|
||||
(and taking? (seq text))
|
||||
(conj (assoc node-style :text text)))
|
||||
continue? (or (> from end) (>= end to))]
|
||||
(recur (when continue? (rest styles)) taking? to result))
|
||||
@ -95,10 +95,11 @@
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
:$page {:enumerable false :get (constantly page-id)}
|
||||
:$start {:enumerable false :get (constantly start)}
|
||||
:$end {:enumerable false :get (constantly end)}
|
||||
|
||||
:shape
|
||||
{:this true
|
||||
:get #(-> % u/proxy->shape)}
|
||||
{:get (fn [] (format/shape-proxy plugin-id file-id page-id id))}
|
||||
|
||||
:characters
|
||||
{:this true
|
||||
|
||||
@ -96,13 +96,83 @@
|
||||
:expand-with-children false})
|
||||
(se/add-event plugin-id))))))
|
||||
|
||||
(defn- typography-resolved-value->js
|
||||
"Converts a resolved typography composite (a Clojure map keyed by the
|
||||
tokenscript field names) into the plugin's `TokenTypographyValue[]` shape: a
|
||||
JS array with a single object using the public camelCase member names."
|
||||
[m]
|
||||
(when (map? m)
|
||||
#js [#js {"fontFamilies" (clj->js (:font-family m))
|
||||
"fontSizes" (:font-size m)
|
||||
"fontWeights" (some-> (:font-weight m) str)
|
||||
"letterSpacing" (:letter-spacing m)
|
||||
"lineHeight" (:line-height m)
|
||||
"textCase" (:text-case m)
|
||||
"textDecoration" (:text-decoration m)}]))
|
||||
|
||||
(defn- shadow-key->camel
|
||||
"Renames a shadow composite field name (kebab string) to its public camelCase
|
||||
member name. The shadow schema is closed; offset-x/offset-y are its only
|
||||
multi-word fields, so the rest (blur, spread, color, inset) pass through."
|
||||
[k]
|
||||
(case k
|
||||
"offset-x" "offsetX"
|
||||
"offset-y" "offsetY"
|
||||
k))
|
||||
|
||||
(defn- shadow-entry->js
|
||||
"Converts one resolved shadow entry (a JS Map of field name -> tokenscript
|
||||
symbol) into a plain JS object using the public member names and the
|
||||
unit-converted values."
|
||||
[^js m]
|
||||
(let [out #js {}]
|
||||
(.forEach m (fn [sym k]
|
||||
(obj/set! out (shadow-key->camel k)
|
||||
(ts/tokenscript-symbols->penpot-unit sym))))
|
||||
out))
|
||||
|
||||
(defn- shadow-resolved-value->js
|
||||
"Converts a resolved shadow composite (a sequence of shadow entries) into the
|
||||
plugin's `TokenShadowValue[]` shape."
|
||||
[entries]
|
||||
(when (some? entries)
|
||||
(into-array (map shadow-entry->js entries))))
|
||||
|
||||
(defn- font-families-resolved-value->js
|
||||
"Converts a resolved fontFamilies value (a tokenscript list symbol) into the
|
||||
documented `string[]` shape rather than leaking the raw tokenscript structure."
|
||||
[resolved-value]
|
||||
(let [v (ts/tokenscript-symbols->penpot-unit resolved-value)]
|
||||
(cond
|
||||
(nil? v) nil
|
||||
(sequential? v) (clj->js v)
|
||||
:else #js [v])))
|
||||
|
||||
(defn- get-resolved-value
|
||||
[token tokens-tree]
|
||||
(let [resolved-tokens (ts/resolve-tokens tokens-tree)
|
||||
resolved-value (-> resolved-tokens
|
||||
(dm/get-in [(:name token) :resolved-value])
|
||||
(ts/tokenscript-symbols->penpot-unit))]
|
||||
resolved-value))
|
||||
resolved-value (dm/get-in resolved-tokens [(:name token) :resolved-value])]
|
||||
(cond
|
||||
(= :font-family (:type token))
|
||||
;; A fontFamilies token resolves to a list of families; expose it as the
|
||||
;; documented `string[]` rather than the raw tokenscript list symbol.
|
||||
(font-families-resolved-value->js resolved-value)
|
||||
|
||||
(= :typography (:type token))
|
||||
;; A typography token resolves to a composite; expose it as the documented
|
||||
;; `TokenTypographyValue[]` rather than the raw tokenscript structure.
|
||||
(typography-resolved-value->js
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
||||
|
||||
(= :shadow (:type token))
|
||||
;; A shadow token resolves to a list of composites whose entries the
|
||||
;; tokenscript unit conversion leaves as raw symbols; expose them as the
|
||||
;; documented `TokenShadowValue[]`.
|
||||
(shadow-resolved-value->js
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
||||
|
||||
:else
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))))
|
||||
|
||||
(defn token-proxy? [p]
|
||||
(obj/type-of? p "TokenProxy"))
|
||||
@ -150,11 +220,21 @@
|
||||
(fn [_]
|
||||
(let [token (u/locate-token file-id set-id id)]
|
||||
(json/->js (:value token))))
|
||||
:schema (let [token (u/locate-token file-id set-id id)]
|
||||
(cfo/make-token-value-schema (:type token)))
|
||||
:schema (let [token (u/locate-token file-id set-id id)
|
||||
base (cfo/make-token-value-schema (:type token))]
|
||||
;; plugin-types declares the fontFamilies value as
|
||||
;; `string | string[]`, but the core schema only accepts a
|
||||
;; vector/ref; also accept a plain string (normalized in :set).
|
||||
(if (= :font-family (:type token))
|
||||
[:or :string base]
|
||||
base))
|
||||
:set
|
||||
(fn [_ value]
|
||||
(st/emit! (dwtl/update-token set-id id {:value value})))}
|
||||
(let [token (u/locate-token file-id set-id id)
|
||||
value (cond-> value
|
||||
(= :font-family (:type token))
|
||||
(ctob/convert-dtcg-font-family))]
|
||||
(st/emit! (dwtl/update-token set-id id {:value value}))))}
|
||||
|
||||
:resolvedValue
|
||||
{:this true
|
||||
@ -361,7 +441,10 @@
|
||||
|
||||
:duplicate
|
||||
(fn []
|
||||
(st/emit! (dwtl/duplicate-token-set id)))
|
||||
(let [id-ref (atom nil)]
|
||||
(st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref}))
|
||||
(when (some? @id-ref)
|
||||
(token-set-proxy plugin-id file-id @id-ref))))
|
||||
|
||||
:remove
|
||||
(fn []
|
||||
@ -460,7 +543,7 @@
|
||||
;; Guard against nil to prevent `enable-set` from conj'ing nil
|
||||
;; into the theme's :sets — which would send `:sets #{nil}` to the
|
||||
;; backend and crash the workspace.
|
||||
(let [set-name (obj/get token-set :name)
|
||||
(let [set-name (obj/get token-set "name")
|
||||
theme (u/locate-token-theme file-id id)]
|
||||
(when (and (some? set-name) (some? theme))
|
||||
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))}
|
||||
@ -470,7 +553,7 @@
|
||||
:schema [:tuple [:fn token-set-proxy?]]
|
||||
:fn (fn [token-set]
|
||||
;; Same nil guard as addSet — see comment above.
|
||||
(let [set-name (obj/get token-set :name)
|
||||
(let [set-name (obj/get token-set "name")
|
||||
theme (u/locate-token-theme file-id id)]
|
||||
(when (and (some? set-name) (some? theme))
|
||||
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))}
|
||||
|
||||
@ -323,14 +323,27 @@
|
||||
message to the console."
|
||||
[plugin-id]
|
||||
(fn [cause]
|
||||
(let [message
|
||||
(if-let [explain (-> cause ex-data ::sm/explain)]
|
||||
(do
|
||||
(js/console.error (sm/humanize-explain explain))
|
||||
(error-messages explain))
|
||||
(ex-data cause))]
|
||||
(js/console.log (.-stack cause))
|
||||
(not-valid plugin-id :error message))))
|
||||
(let [explain (-> cause ex-data ::sm/explain)
|
||||
throw? (throw-validation-errors? plugin-id)]
|
||||
(cond
|
||||
;; If it's a clojure error we throw as a validation error
|
||||
(and throw? explain)
|
||||
(throw-not-valid :error (error-messages explain))
|
||||
|
||||
;; Unexpected errors we just propagate them
|
||||
throw?
|
||||
(throw cause)
|
||||
|
||||
;; If not throw is active we log the caught error
|
||||
:else
|
||||
(let [message
|
||||
(if explain
|
||||
(do
|
||||
(js/console.error (sm/humanize-explain explain))
|
||||
(error-messages explain))
|
||||
(ex-data cause))]
|
||||
(js/console.log (.-stack cause))
|
||||
(not-valid plugin-id :error message))))))
|
||||
|
||||
(defn is-main-component-proxy?
|
||||
[p]
|
||||
|
||||
@ -1073,66 +1073,75 @@
|
||||
|
||||
(defn set-grid-layout-rows
|
||||
[entries]
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
;; Only allocate when there are entries; an empty list would alloc 0 bytes.
|
||||
;; The wasm side reads an empty buffer as zero rows.
|
||||
(when (seq entries)
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE)))
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE)))
|
||||
|
||||
offset
|
||||
entries)
|
||||
offset
|
||||
entries)))
|
||||
|
||||
(h/call wasm/internal-module "_set_grid_rows")))
|
||||
(h/call wasm/internal-module "_set_grid_rows"))
|
||||
|
||||
(defn set-grid-layout-columns
|
||||
[entries]
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
;; Only allocate when there are entries; an empty list would alloc 0 bytes.
|
||||
;; The wasm side reads an empty buffer as zero columns.
|
||||
(when (seq entries)
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE)))
|
||||
offset
|
||||
entries)
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE)))
|
||||
offset
|
||||
entries)))
|
||||
|
||||
(h/call wasm/internal-module "_set_grid_columns")))
|
||||
(h/call wasm/internal-module "_set_grid_columns"))
|
||||
|
||||
(defn set-grid-layout-cells
|
||||
[cells]
|
||||
(let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
;; Only allocate when there are cells; an empty collection would alloc 0
|
||||
;; bytes. The wasm side reads an empty buffer as zero cells.
|
||||
(when (seq cells)
|
||||
(let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
|
||||
(reduce-kv (fn [offset _ cell]
|
||||
(let [shape-id (-> (get cell :shapes) first)]
|
||||
(-> offset
|
||||
(mem/write-i32 dview (get cell :row))
|
||||
(mem/write-i32 dview (get cell :row-span))
|
||||
(mem/write-i32 dview (get cell :column))
|
||||
(mem/write-i32 dview (get cell :column-span))
|
||||
(reduce-kv (fn [offset _ cell]
|
||||
(let [shape-id (-> (get cell :shapes) first)]
|
||||
(-> offset
|
||||
(mem/write-i32 dview (get cell :row))
|
||||
(mem/write-i32 dview (get cell :row-span))
|
||||
(mem/write-i32 dview (get cell :column))
|
||||
(mem/write-i32 dview (get cell :column-span))
|
||||
|
||||
(mem/write-u8 dview (sr/translate-align-self (get cell :align-self)))
|
||||
(mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self)))
|
||||
(mem/write-u8 dview (sr/translate-align-self (get cell :align-self)))
|
||||
(mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self)))
|
||||
|
||||
;; padding
|
||||
(+ 2)
|
||||
;; padding
|
||||
(+ 2)
|
||||
|
||||
(mem/write-uuid dview (d/nilv shape-id uuid/zero))
|
||||
(mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE))))
|
||||
(mem/write-uuid dview (d/nilv shape-id uuid/zero))
|
||||
(mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE))))
|
||||
|
||||
offset
|
||||
cells)
|
||||
offset
|
||||
cells)))
|
||||
|
||||
(h/call wasm/internal-module "_set_grid_cells")))
|
||||
(h/call wasm/internal-module "_set_grid_cells"))
|
||||
|
||||
(defn set-grid-layout
|
||||
[shape]
|
||||
|
||||
@ -131,6 +131,39 @@
|
||||
[_ms]
|
||||
(rx/of :immediate))
|
||||
|
||||
;; Static-dispatch-safe stubs
|
||||
;; ═══════════════════════════════════════════════════════════════
|
||||
;;
|
||||
;; The `:esm` test build compiles calls to a *multi-arity* var as
|
||||
;; `f.cljs$core$IFn$_invoke$arity$N(...)`. A plain single-arity `fn`
|
||||
;; (including `identity`) does not expose that property, so using one
|
||||
;; to redefine such a var throws "arity$N is not a function". Multi-arity
|
||||
;; fns do expose the property, hence the helpers below.
|
||||
|
||||
(defn noop
|
||||
"Multi-arity no-op. Use to stub static-dispatched multi-arity vars
|
||||
such as `st/emit!` (replacing `identity`, which is single-arity)."
|
||||
([] nil)
|
||||
([_] nil)
|
||||
([_ _] nil)
|
||||
([_ _ _] nil)
|
||||
([_ _ _ _] nil)
|
||||
([_ _ _ _ & _] nil))
|
||||
|
||||
(defn stub
|
||||
"Wraps `f` in a multi-arity fn (arities 0-6) delegating to `f`, so the
|
||||
result exposes `cljs$core$IFn$_invoke$arity$N`. Required when replacing
|
||||
a multi-arity var in a `with-redefs`/`set!` mock with a capturing fn."
|
||||
[f]
|
||||
(fn
|
||||
([] (f))
|
||||
([a] (f a))
|
||||
([a b] (f a b))
|
||||
([a b c] (f a b c))
|
||||
([a b c d] (f a b c d))
|
||||
([a b c d e] (f a b c d e))
|
||||
([a b c d e g] (f a b c d e g))))
|
||||
|
||||
;; Lifecycle
|
||||
;; ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
60
frontend/test/frontend_tests/plugins/comments_test.cljs
Normal file
60
frontend/test/frontend_tests/plugins/comments_test.cljs
Normal file
@ -0,0 +1,60 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.comments-test
|
||||
(:require
|
||||
[app.main.data.comments :as dc]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.comments :as comments]
|
||||
[app.plugins.page :as page]
|
||||
[app.plugins.register :as r]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(t/deftest comment-thread-remove-allows-the-owner
|
||||
(let [owner-id (random-uuid)
|
||||
file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
thread-id (random-uuid)
|
||||
emitted (atom nil)
|
||||
thread (comments/comment-thread-proxy
|
||||
plugin-id
|
||||
file-id
|
||||
page-id
|
||||
{:id thread-id :owner-id owner-id})]
|
||||
(set! st/state (atom {:profile {:id owner-id}}))
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
dc/delete-comment-thread-on-workspace
|
||||
(mock/stub (fn [params callback]
|
||||
(callback)
|
||||
[:delete-thread params]))
|
||||
st/emit! (mock/stub (fn [event] (reset! emitted event)))]
|
||||
(let [result (.remove thread)]
|
||||
(t/is (instance? js/Promise result))
|
||||
(t/is (= [:delete-thread {:id thread-id}] @emitted))))))
|
||||
|
||||
(t/deftest page-remove-comment-thread-emits-delete-event
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
thread-id (random-uuid)
|
||||
emitted (atom nil)
|
||||
page (page/page-proxy plugin-id file-id page-id)
|
||||
thread (comments/comment-thread-proxy
|
||||
plugin-id
|
||||
file-id
|
||||
page-id
|
||||
{:id thread-id :owner-id (random-uuid)})]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
dc/delete-comment-thread-on-workspace
|
||||
(mock/stub (fn [params callback]
|
||||
(callback)
|
||||
[:delete-thread params]))
|
||||
st/emit! (mock/stub (fn [event] (reset! emitted event)))]
|
||||
(let [result (.removeCommentThread page thread)]
|
||||
(t/is (instance? js/Promise result))
|
||||
(t/is (= [:delete-thread {:id thread-id}] @emitted))))))
|
||||
@ -14,7 +14,8 @@
|
||||
[app.util.object :as obj]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/deftest test-common-shape-properties
|
||||
(thw/with-wasm-mocks*
|
||||
@ -25,6 +26,7 @@
|
||||
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
_ (set! st/state store)
|
||||
_ (ptk/emit! store #(assoc-in % [:plugins :flags "00000000-0000-0000-0000-000000000000" :throw-validation-errors] true))
|
||||
|
||||
^js file (. context -currentFile)
|
||||
^js page (. context -currentPage)
|
||||
@ -65,7 +67,7 @@
|
||||
(t/is (= (.-x shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :x)) 10))
|
||||
|
||||
(set! (.-x shape) "fail")
|
||||
(t/is (thrown? js/Error (set! (.-x shape) "fail")))
|
||||
(t/is (= (.-x shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :x)) 10)))
|
||||
|
||||
@ -74,7 +76,7 @@
|
||||
(t/is (= (.-y shape) 50))
|
||||
(t/is (= (get-in @store (get-shape-path :y)) 50))
|
||||
|
||||
(set! (.-y shape) "fail")
|
||||
(t/is (thrown? js/Error (set! (.-y shape) "fail")))
|
||||
(t/is (= (.-y shape) 50))
|
||||
(t/is (= (get-in @store (get-shape-path :y)) 50)))
|
||||
|
||||
@ -85,7 +87,7 @@
|
||||
(t/is (= (get-in @store (get-shape-path :width)) 250))
|
||||
(t/is (= (get-in @store (get-shape-path :height)) 300))
|
||||
|
||||
(.resize shape 0 0)
|
||||
(t/is (thrown? js/Error (.resize shape 0 0)))
|
||||
(t/is (= (.-width shape) 250))
|
||||
(t/is (= (.-height shape) 300))
|
||||
(t/is (= (get-in @store (get-shape-path :width)) 250))
|
||||
@ -115,7 +117,7 @@
|
||||
(t/is (= (get-in @store (get-shape-path :proportion-lock)) true)))
|
||||
|
||||
(t/testing " - constraintsHorizontal"
|
||||
(set! (.-constraintsHorizontal shape) "fail")
|
||||
(t/is (thrown? js/Error (set! (.-constraintsHorizontal shape) "fail")))
|
||||
(t/is (not= (.-constraintsHorizontal shape) "fail"))
|
||||
(t/is (not= (get-in @store (get-shape-path :constraints-h)) "fail"))
|
||||
|
||||
@ -124,7 +126,7 @@
|
||||
(t/is (= (get-in @store (get-shape-path :constraints-h)) :right)))
|
||||
|
||||
(t/testing " - constraintsVertical"
|
||||
(set! (.-constraintsVertical shape) "fail")
|
||||
(t/is (thrown? js/Error (set! (.-constraintsVertical shape) "fail")))
|
||||
(t/is (not= (.-constraintsVertical shape) "fail"))
|
||||
(t/is (not= (get-in @store (get-shape-path :constraints-v)) "fail"))
|
||||
|
||||
@ -175,7 +177,7 @@
|
||||
(t/is (= (.-blendMode shape) "multiply"))
|
||||
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply))
|
||||
|
||||
(set! (.-blendMode shape) "fail")
|
||||
(t/is (thrown? js/Error (set! (.-blendMode shape) "fail")))
|
||||
(t/is (= (.-blendMode shape) "multiply"))
|
||||
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply)))
|
||||
|
||||
@ -194,7 +196,7 @@
|
||||
:color {:color "#fabada" :opacity 1}
|
||||
:hidden false}]))))
|
||||
(let [shadow #js {:style "fail"}]
|
||||
(set! (.-shadows shape) #js [shadow])
|
||||
(t/is (thrown? js/Error (set! (.-shadows shape) #js [shadow])))
|
||||
(t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow"))))
|
||||
|
||||
(t/testing " - blur"
|
||||
@ -211,7 +213,7 @@
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test"))
|
||||
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}]))
|
||||
|
||||
(set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])
|
||||
(t/is (thrown? js/Error (set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])))
|
||||
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}])))
|
||||
|
||||
(t/testing " - flipX"
|
||||
@ -234,7 +236,7 @@
|
||||
(t/is (= (get-in @store (get-shape-path :rotation)) 0)))
|
||||
|
||||
(t/testing " - fills"
|
||||
(set! (.-fills shape) #js [#js {:fillColor 100}])
|
||||
(t/is (thrown? js/Error (set! (.-fills shape) #js [#js {:fillColor 100}])))
|
||||
(t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#B1B2B5" :fill-opacity 1}]))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5"))
|
||||
|
||||
|
||||
21
frontend/test/frontend_tests/plugins/file_test.cljs
Normal file
21
frontend/test/frontend_tests/plugins/file_test.cljs
Normal file
@ -0,0 +1,21 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.file-test
|
||||
(:require
|
||||
[app.plugins.file :as file]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/deftest file-version-created-at-returns-stored-date
|
||||
(let [created-at (js/Date.)
|
||||
version (file/file-version-proxy
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
(random-uuid)
|
||||
{}
|
||||
{:id (random-uuid)
|
||||
:label "Version"
|
||||
:created-at created-at})]
|
||||
(t/is (identical? created-at (.-createdAt version)))))
|
||||
@ -38,3 +38,17 @@
|
||||
(format/format-frame-guides nil)
|
||||
(format/format-tracks nil)
|
||||
(format/format-path-content nil)))
|
||||
|
||||
(t/deftest test-format-color-result-uses-shapes-info-key
|
||||
(let [shape-id (random-uuid)
|
||||
result (format/format-color-result
|
||||
[{:color "#fabada"}
|
||||
[{:prop :fill :shape-id shape-id :index 0}]])
|
||||
info (aget result "shapesInfo")]
|
||||
(t/is (array? info))
|
||||
(t/is (nil? (aget result "shapesColors")))
|
||||
(t/is (= "fill" (aget (aget info 0) "property")))
|
||||
(t/is (= (str shape-id) (aget (aget info 0) "shapeId")))))
|
||||
|
||||
(t/deftest test-shape-type-reports-boolean
|
||||
(t/is (= "boolean" (format/shape-type :bool))))
|
||||
|
||||
49
frontend/test/frontend_tests/plugins/grid_test.cljs
Normal file
49
frontend/test/frontend_tests/plugins/grid_test.cljs
Normal 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.grid-test
|
||||
(:require
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(defn- setup-grid []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
_ (set! st/state store)
|
||||
_ (set! st/stream (ptk/input-stream store))
|
||||
context (api/create-context plugin-id)
|
||||
board (.createBoard ^js context)
|
||||
grid (.addGridLayout ^js board)]
|
||||
{:store store :context context :board board :grid grid}))
|
||||
|
||||
(t/deftest add-column-at-index-accepts-fixed-track-type
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [{:keys [^js grid]} (setup-grid)]
|
||||
(.addColumn grid "flex" 1)
|
||||
(.addColumnAtIndex grid 0 "fixed" 100)
|
||||
(t/is (= "fixed" (aget (aget (.-columns grid) 0) "type")))
|
||||
(t/is (= 100 (aget (aget (.-columns grid) 0) "value")))))))
|
||||
|
||||
(t/deftest grid-track-methods-reject-out-of-range-indices
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [{:keys [store ^js grid]} (setup-grid)]
|
||||
(swap! store assoc-in [:plugins :flags plugin-id :throw-validation-errors] true)
|
||||
(.addRow grid "flex" 1)
|
||||
(.addColumn grid "flex" 1)
|
||||
(t/is (thrown? js/Error (.addRowAtIndex grid -1 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.addColumnAtIndex grid 2 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.setRow grid 1 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.setColumn grid 1 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.removeRow grid 1)))
|
||||
(t/is (thrown? js/Error (.removeColumn grid 1)))))))
|
||||
95
frontend/test/frontend_tests/plugins/library_test.cljs
Normal file
95
frontend/test/frontend_tests/plugins/library_test.cljs
Normal file
@ -0,0 +1,95 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.library-test
|
||||
(:require
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.library :as library]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.text :as text]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(t/deftest library-asset-proxies-expose-library-id
|
||||
(let [file-id (random-uuid)
|
||||
id (random-uuid)]
|
||||
(t/is (= (str file-id) (.-libraryId (library/lib-color-proxy plugin-id file-id id))))
|
||||
(t/is (= (str file-id) (.-libraryId (library/lib-typography-proxy plugin-id file-id id))))
|
||||
(t/is (= (str file-id) (.-libraryId (library/lib-component-proxy plugin-id file-id id))))))
|
||||
|
||||
(t/deftest typography-apply-to-text-range-uses-hidden-range-bounds
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
typography-id (random-uuid)
|
||||
typography (library/lib-typography-proxy plugin-id file-id typography-id)
|
||||
text-range (text/text-range-proxy plugin-id file-id page-id shape-id 2 5)
|
||||
captured (atom nil)]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/page-active? (constantly true)
|
||||
u/locate-library-typography
|
||||
(constantly {:id typography-id
|
||||
:name "Body"
|
||||
:font-size "14"})
|
||||
dwt/update-text-range
|
||||
(fn [shape-id start end attrs]
|
||||
(reset! captured {:shape-id shape-id
|
||||
:start start
|
||||
:end end
|
||||
:attrs attrs})
|
||||
:update-text-range)
|
||||
st/emit! mock/noop]
|
||||
(.applyToTextRange typography text-range)
|
||||
(t/is (= shape-id (:shape-id @captured)))
|
||||
(t/is (= 2 (:start @captured)))
|
||||
(t/is (= 5 (:end @captured)))
|
||||
(t/is (= file-id (get-in @captured [:attrs :typography-ref-file])))
|
||||
(t/is (= typography-id (get-in @captured [:attrs :typography-ref-id]))))))
|
||||
|
||||
(t/deftest library-color-gradient-and-image-clear-exclusive-representations
|
||||
(let [file-id (random-uuid)
|
||||
color-id (random-uuid)
|
||||
proxy (library/lib-color-proxy plugin-id file-id color-id)
|
||||
captured (atom nil)
|
||||
base {:id color-id
|
||||
:name "Brand"
|
||||
:color "#fabada"
|
||||
:opacity 1
|
||||
:gradient {:type :linear}
|
||||
:image {:id (random-uuid) :width 1 :height 1}}]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/proxy->library-color (constantly base)
|
||||
dwl/update-color-data (fn [color file-id]
|
||||
(reset! captured {:color color :file-id file-id})
|
||||
:update-color-data)
|
||||
st/emit! mock/noop]
|
||||
(set! (.-gradient proxy)
|
||||
#js {:type "linear"
|
||||
:startX 0
|
||||
:startY 0
|
||||
:endX 1
|
||||
:endY 1
|
||||
:width 1
|
||||
:stops #js [#js {:color "#000000"
|
||||
:opacity 1
|
||||
:offset 0}]})
|
||||
(t/is (contains? (:color @captured) :gradient))
|
||||
(t/is (not (contains? (:color @captured) :color)))
|
||||
(t/is (not (contains? (:color @captured) :image)))
|
||||
|
||||
(set! (.-image proxy)
|
||||
#js {:id (str (random-uuid))
|
||||
:width 10
|
||||
:height 20
|
||||
:mtype "image/png"})
|
||||
(t/is (contains? (:color @captured) :image))
|
||||
(t/is (not (contains? (:color @captured) :color)))
|
||||
(t/is (not (contains? (:color @captured) :gradient))))))
|
||||
28
frontend/test/frontend_tests/plugins/local_storage_test.cljs
Normal file
28
frontend/test/frontend_tests/plugins/local_storage_test.cljs
Normal file
@ -0,0 +1,28 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.local-storage-test
|
||||
(:require
|
||||
[app.plugins.local-storage :as storage]
|
||||
[app.plugins.register :as r]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/deftest remove-item-removes-the-prefixed-key
|
||||
(let [data (atom {})
|
||||
fake #js {}
|
||||
plugin-id "plugin-a"]
|
||||
(set! (.-getItem fake) (fn [key] (get @data key)))
|
||||
(set! (.-setItem fake) (fn [key value] (swap! data assoc key value)))
|
||||
(set! (.-removeItem fake) (fn [key] (swap! data dissoc key)))
|
||||
(set! (.-keys fake) (fn [] (to-array (keys @data))))
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
storage/local-storage fake]
|
||||
(let [proxy (storage/local-storage-proxy plugin-id)]
|
||||
(.setItem proxy "key" "value")
|
||||
(t/is (= "value" (.getItem proxy "key")))
|
||||
(.removeItem proxy "key")
|
||||
(t/is (nil? (.getItem proxy "key")))
|
||||
(t/is (empty? @data))))))
|
||||
@ -32,6 +32,7 @@
|
||||
store (ths/setup-store file)
|
||||
_ (set! st/state store)
|
||||
_ (set! st/stream (ptk/input-stream store))
|
||||
_ (ptk/emit! store #(assoc-in % [:plugins :flags "00000000-0000-0000-0000-000000000000" :throw-validation-errors] true))
|
||||
context (api/create-context "00000000-0000-0000-0000-000000000000")]
|
||||
{:file file :store store :context context}))
|
||||
|
||||
@ -89,9 +90,11 @@
|
||||
^js page2 (aget pages 1)]
|
||||
(t/is (instance? js/Promise (.openPage context page2 true)))))
|
||||
|
||||
(t/deftest test-open-page-invalid-arg-returns-nil
|
||||
(t/deftest test-open-page-invalid-arg-throws
|
||||
;; With throwValidationErrors enabled an invalid argument surfaces as an
|
||||
;; exception instead of being silently logged.
|
||||
(let [^js context (:context (setup))]
|
||||
(t/is (nil? (.openPage context "not-a-page")))))
|
||||
(t/is (thrown? js/Error (.openPage context "not-a-page")))))
|
||||
|
||||
(t/deftest test-open-page-resolves-when-page-changes
|
||||
(t/async done
|
||||
|
||||
@ -31,3 +31,33 @@
|
||||
(t/is (gpt/point? result))
|
||||
(t/is (= 0 (:x result)))
|
||||
(t/is (= 0 (:y result))))))
|
||||
|
||||
(t/deftest test-parse-overlay-action-defaults-manual-position
|
||||
(let [destination #js {"$id" (random-uuid)}
|
||||
action (parser/parse-action
|
||||
#js {:type "open-overlay"
|
||||
:destination destination
|
||||
:position "center"})]
|
||||
(t/is (= :open-overlay (:action-type action)))
|
||||
(t/is (= :center (:overlay-pos-type action)))
|
||||
(t/is (gpt/point? (:overlay-position action)))
|
||||
(t/is (= 0 (:x (:overlay-position action))))
|
||||
(t/is (= 0 (:y (:overlay-position action))))))
|
||||
|
||||
(t/deftest test-parse-frame-guide-calls-guide-parser
|
||||
(let [column (parser/parse-frame-guide
|
||||
#js {:type "column"
|
||||
:display true
|
||||
:params #js {:type "stretch"
|
||||
:size 12}})
|
||||
row (parser/parse-frame-guide
|
||||
#js {:type "row"
|
||||
:display false
|
||||
:params #js {:type "center"
|
||||
:margin 4}})]
|
||||
(t/is (= :column (:type column)))
|
||||
(t/is (= true (:display column)))
|
||||
(t/is (= :stretch (get-in column [:params :type])))
|
||||
(t/is (= :row (:type row)))
|
||||
(t/is (= false (:display row)))
|
||||
(t/is (= :center (get-in row [:params :type])))))
|
||||
|
||||
202
frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs
Normal file
202
frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs
Normal file
@ -0,0 +1,202 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.shape-bugfixes-test
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[app.plugins.public-utils :as public-utils]
|
||||
[app.plugins.shape :as shape]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- child-shapes
|
||||
"Ordered child shape ids of `board`, read back from the live store
|
||||
(the observable result of a z-order operation)."
|
||||
[store ^js context ^js board]
|
||||
(let [file-id (aget (. context -currentFile) "$id")
|
||||
page-id (aget (. context -currentPage) "$id")
|
||||
board-id (aget board "$id")]
|
||||
(get-in @store [:files file-id :data :pages-index page-id
|
||||
:objects board-id :shapes])))
|
||||
|
||||
(defn- page-guides
|
||||
"The guides map of the current page, read back from the live store."
|
||||
[store ^js context]
|
||||
(let [file-id (aget (. context -currentFile) "$id")
|
||||
page-id (aget (. context -currentPage) "$id")]
|
||||
(get-in @store [:files file-id :data :pages-index page-id :guides])))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest trigger-setter-updates-the-interaction-event-type
|
||||
;; Regression: the `trigger` setter must update the interaction of the
|
||||
;; located shape. Asserting on the observable interaction (read back through
|
||||
;; the proxy from the live store) covers that without coupling to which
|
||||
;; internal action gets emitted.
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context plugin-id)
|
||||
_ (set! st/state store)
|
||||
^js board (.createBoard context)]
|
||||
(.addInteraction board "click" #js {:type "open-url" :url "https://example.com"})
|
||||
(let [^js interaction (aget (.-interactions board) 0)]
|
||||
(t/is (= "click" (.-trigger interaction))
|
||||
"the interaction starts with the click trigger")
|
||||
(set! (.-trigger interaction) "mouse-over")
|
||||
(t/is (= "mouse-over" (.-trigger interaction))
|
||||
"the trigger setter updates the interaction event-type"))))))
|
||||
|
||||
(t/deftest center-shapes-empty-input-returns-nil
|
||||
(t/is (nil? (public-utils/centerShapes #js []))))
|
||||
|
||||
(t/deftest background-blur-reads-background-blur-key
|
||||
(let [file-id (uuid/next)
|
||||
page-id (uuid/next)
|
||||
shape-id (uuid/next)
|
||||
blur-id (uuid/next)
|
||||
proxy (shape/shape-proxy plugin-id file-id page-id shape-id)]
|
||||
(with-redefs [u/proxy->shape (constantly {:background-blur {:id blur-id
|
||||
:value 12
|
||||
:hidden false}})]
|
||||
(let [blur (.-backgroundBlur proxy)]
|
||||
(t/is (= (str blur-id) (aget blur "id")))
|
||||
(t/is (= 12 (aget blur "value")))))))
|
||||
|
||||
(t/deftest flatten-returns-proxies-for-converted-shapes
|
||||
;; `convert-selected-to-path` runs the WASM boolean/path pipeline, so this
|
||||
;; test stays at the proxy boundary: it verifies `flatten` forwards the
|
||||
;; selected ids to the conversion and wraps the result back into proxies.
|
||||
(let [file-id (uuid/next)
|
||||
page-id (uuid/next)
|
||||
shape-id (uuid/next)
|
||||
input (shape/shape-proxy plugin-id file-id page-id shape-id)
|
||||
emitted (atom nil)
|
||||
context (api/create-context plugin-id)]
|
||||
(set! st/state (atom {:current-file-id file-id
|
||||
:current-page-id page-id}))
|
||||
(with-redefs [dw/convert-selected-to-path
|
||||
(mock/stub (fn [ids]
|
||||
(reset! emitted ids)
|
||||
:convert-selected-to-path))
|
||||
st/emit! mock/noop
|
||||
shape/shape-proxy
|
||||
(mock/stub (fn [_plugin file page id]
|
||||
#js {"$file" file "$page" page "$id" id}))]
|
||||
(let [result (.flatten context #js [input])]
|
||||
(t/is (= #{shape-id} @emitted))
|
||||
(t/is (array? result))
|
||||
(t/is (= shape-id (aget result 0 "$id")))
|
||||
(t/is (= file-id (aget result 0 "$file")))
|
||||
(t/is (= page-id (aget result 0 "$page")))))))
|
||||
|
||||
(t/deftest z-order-methods-reorder-the-shape-within-its-parent
|
||||
;; Asserts the observable child order in the parent after each z-order
|
||||
;; method, instead of merely checking which location keyword was emitted.
|
||||
;; The assertions are independent of the parent's `:shapes` ordering
|
||||
;; convention: a reorder is verified by relative movement and extremes.
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context plugin-id)
|
||||
_ (set! st/state store)
|
||||
^js board (.createBoard context)
|
||||
children (mapv (fn [_] (.createRectangle context)) (range 4))
|
||||
ids (mapv #(aget % "$id") children)
|
||||
order #(child-shapes store context board)]
|
||||
(doseq [^js c children] (.appendChild board c))
|
||||
|
||||
;; Operate on a shape that is currently interior (so both a forward
|
||||
;; and a backward step are observable).
|
||||
(let [mid-id (nth (order) 1)
|
||||
^js mid (nth children (d/index-of ids mid-id))]
|
||||
|
||||
(t/testing "bringForward and sendBackward move in opposite directions"
|
||||
(let [i0 (d/index-of (order) mid-id)
|
||||
_ (.bringForward mid)
|
||||
i1 (d/index-of (order) mid-id)
|
||||
_ (.sendBackward mid)
|
||||
i2 (d/index-of (order) mid-id)]
|
||||
(t/is (not= i0 i1) "bringForward changes the order")
|
||||
(t/is (not= i1 i2) "sendBackward changes the order")
|
||||
(t/is (= (pos? (- i1 i0)) (neg? (- i2 i1)))
|
||||
"the two steps move the shape in opposite directions")))
|
||||
|
||||
(t/testing "bringToFront and sendToBack move to opposite extremes"
|
||||
(let [n (count (order))
|
||||
_ (.bringToFront mid)
|
||||
p1 (d/index-of (order) mid-id)
|
||||
_ (.sendToBack mid)
|
||||
p2 (d/index-of (order) mid-id)]
|
||||
(t/is (contains? #{0 (dec n)} p1) "bringToFront moves to an extreme")
|
||||
(t/is (contains? #{0 (dec n)} p2) "sendToBack moves to an extreme")
|
||||
(t/is (not= p1 p2) "front and back are opposite extremes"))))))))
|
||||
|
||||
(t/deftest is-variant-container-predicate-returns-boolean
|
||||
(t/is (false? (ctk/is-variant-container? {})))
|
||||
(t/is (true? (ctk/is-variant-container? {:is-variant-container true}))))
|
||||
|
||||
(t/deftest combine-as-variants-uses-the-passed-component-ids
|
||||
;; `combine-as-variants` needs real main components and the variant pipeline,
|
||||
;; so this stays at the proxy boundary and verifies the component ids that
|
||||
;; the head proxy collects from its argument before delegating.
|
||||
(let [file-id (uuid/next)
|
||||
page-id (uuid/next)
|
||||
head-id (uuid/next)
|
||||
other-id (uuid/next)
|
||||
proxy (shape/shape-proxy plugin-id file-id page-id head-id)
|
||||
captured (atom nil)]
|
||||
(with-redefs [u/locate-shape (fn [_file _page id] {:id id :component-id id})
|
||||
u/locate-library-component (constantly {:id (uuid/next)})
|
||||
ctk/is-variant? (constantly false)
|
||||
dwv/combine-as-variants
|
||||
(fn [ids opts]
|
||||
(reset! captured {:ids ids :opts opts})
|
||||
;; return value flows through `se/add-event` (which
|
||||
;; calls `with-meta`), so it must support metadata
|
||||
{:event :combine-as-variants})
|
||||
st/emit! mock/noop
|
||||
shape/shape-proxy (mock/stub (fn [& _] #js {}))]
|
||||
(.combineAsVariants proxy #js [(str other-id)])
|
||||
(t/is (= #{head-id other-id} (:ids @captured))))))
|
||||
|
||||
(t/deftest remove-ruler-guide-deletes-the-guide-from-the-page
|
||||
;; Adds a real ruler guide through the API and asserts it is gone from the
|
||||
;; page guides after removeRulerGuide, rather than checking the removal call.
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context plugin-id)
|
||||
_ (set! st/state store)
|
||||
^js board (.createBoard context)
|
||||
^js guide (.addRulerGuide board "horizontal" 10)]
|
||||
(t/is (= 1 (count (page-guides store context)))
|
||||
"addRulerGuide stores one guide on the page")
|
||||
(.removeRulerGuide board guide)
|
||||
(t/is (empty? (page-guides store context))
|
||||
"removeRulerGuide deletes the guide from the page")))))
|
||||
|
||||
(t/deftest group-empty-input-returns-nil
|
||||
(let [context (api/create-context plugin-id)]
|
||||
(t/is (nil? (.group context #js [])))))
|
||||
@ -6,8 +6,18 @@
|
||||
|
||||
(ns frontend-tests.plugins.text-test
|
||||
(:require
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.fonts :as fonts]
|
||||
[app.plugins.format :as format]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.shape :as shape]
|
||||
[app.plugins.text :as plugins.text]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
;; Regression coverage for issue #9780.
|
||||
;;
|
||||
@ -35,3 +45,71 @@
|
||||
(t/is (not (valid? "abc")))
|
||||
(t/is (not (valid? "1-2")))
|
||||
(t/is (not (valid? "--1"))))
|
||||
|
||||
|
||||
(t/deftest font-apply-to-text-uses-font-id-not-shape-id
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
font (fonts/font-proxy
|
||||
plugin-id
|
||||
{:id "font-id"
|
||||
:family "Inter"
|
||||
:name "Inter"
|
||||
:variants [{:id "regular"
|
||||
:name "Regular"
|
||||
:weight "400"
|
||||
:style "normal"}]})
|
||||
text (shape/shape-proxy plugin-id file-id page-id shape-id)
|
||||
captured (atom nil)]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/page-active? (constantly true)
|
||||
dwt/update-attrs
|
||||
(fn [id attrs]
|
||||
(reset! captured {:id id :attrs attrs})
|
||||
:update-attrs)
|
||||
st/emit! mock/noop]
|
||||
(.applyToText font text nil)
|
||||
(t/is (= shape-id (:id @captured)))
|
||||
(t/is (= "font-id" (get-in @captured [:attrs :font-id]))))))
|
||||
|
||||
(t/deftest font-apply-to-range-uses-hidden-range-bounds
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
font (fonts/font-proxy
|
||||
plugin-id
|
||||
{:id "font-id"
|
||||
:family "Inter"
|
||||
:name "Inter"
|
||||
:variants [{:id "regular"
|
||||
:name "Regular"
|
||||
:weight "400"
|
||||
:style "normal"}]})
|
||||
range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 1 4)
|
||||
captured (atom nil)]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/page-active? (constantly true)
|
||||
dwt/update-text-range
|
||||
(fn [id start end attrs]
|
||||
(reset! captured {:id id
|
||||
:start start
|
||||
:end end
|
||||
:attrs attrs})
|
||||
:update-text-range)
|
||||
st/emit! mock/noop]
|
||||
(.applyToRange font range nil)
|
||||
(t/is (= shape-id (:id @captured)))
|
||||
(t/is (= 1 (:start @captured)))
|
||||
(t/is (= 4 (:end @captured)))
|
||||
(t/is (= "font-id" (get-in @captured [:attrs :font-id]))))))
|
||||
|
||||
(t/deftest text-range-shape-returns-a-shape-proxy
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 0 3)]
|
||||
(with-redefs [format/shape-proxy shape/shape-proxy]
|
||||
(let [text-shape (.-shape range)]
|
||||
(t/is (shape/shape-proxy? text-shape))
|
||||
(t/is (= shape-id (aget text-shape "$id")))))))
|
||||
|
||||
@ -12,15 +12,20 @@
|
||||
[app.common.test-helpers.tokens :as ctht]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[app.plugins.tokens :as ptok]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each {:before cthi/reset-idmap!})
|
||||
|
||||
(def ^:private get-resolved-value @#'ptok/get-resolved-value)
|
||||
|
||||
;; Regression coverage for issue #9162.
|
||||
;;
|
||||
;; Plugin code calling `shape.applyToken(token, ["fill"])` or
|
||||
@ -226,3 +231,110 @@
|
||||
{:keys [errors resolved-value]} (get resolved (:name token))]
|
||||
(t/is (nil? resolved-value))
|
||||
(t/is (seq errors))))
|
||||
|
||||
(t/deftest token-set-duplicate-returns-the-duplicated-set
|
||||
(let [file-id (cthi/new-id! :file)
|
||||
set-id (cthi/new-id! :set)
|
||||
dup-id (cthi/new-id! :dup)
|
||||
proxy (ptok/token-set-proxy "plugin-id" file-id set-id)]
|
||||
(with-redefs [dwtl/duplicate-token-set
|
||||
(mock/stub (fn [id {:keys [id-ref]}]
|
||||
(t/is (= set-id id))
|
||||
(reset! id-ref dup-id)
|
||||
:duplicate-token-set))
|
||||
st/emit! mock/noop]
|
||||
(let [dup (.duplicate proxy)]
|
||||
(t/is (ptok/token-set-proxy? dup))
|
||||
(t/is (= (str dup-id) (.-id dup)))))))
|
||||
|
||||
(t/deftest theme-add-set-and-remove-set-use-the-set-name
|
||||
(let [file-id (cthi/new-id! :file)
|
||||
theme-id (cthi/new-id! :theme)
|
||||
set-id (cthi/new-id! :set)
|
||||
set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives")
|
||||
theme (ptok/token-theme-proxy "plugin-id" file-id theme-id)
|
||||
captured (atom [])]
|
||||
(with-redefs [u/locate-token-theme
|
||||
(fn [_file _theme]
|
||||
(ctob/make-token-theme :id theme-id
|
||||
:name "Theme"
|
||||
:sets #{"Primitives"}))
|
||||
dwtl/update-token-theme
|
||||
(fn [id theme]
|
||||
(swap! captured conj {:id id :theme theme})
|
||||
:update-token-theme)
|
||||
st/emit! identity]
|
||||
(.addSet theme set)
|
||||
(.removeSet theme set)
|
||||
(t/is (= [theme-id theme-id] (mapv :id @captured)))
|
||||
(t/is (contains? (-> @captured first :theme :sets) "Primitives"))
|
||||
(t/is (not (contains? (-> @captured second :theme :sets) "Primitives"))))))
|
||||
|
||||
(t/deftest font-family-token-value-accepts-a-string
|
||||
(let [file-id (cthi/new-id! :file)
|
||||
set-id (cthi/new-id! :set)
|
||||
token-id (cthi/new-id! :token)
|
||||
captured (atom nil)]
|
||||
(with-redefs [u/locate-token (constantly {:id token-id
|
||||
:name "font.primary"
|
||||
:type :font-family
|
||||
:value ["Inter"]})
|
||||
dwtl/update-token (mock/stub (fn [set-id token-id attrs]
|
||||
(reset! captured {:set-id set-id
|
||||
:token-id token-id
|
||||
:attrs attrs})
|
||||
:update-token))
|
||||
st/emit! mock/noop]
|
||||
(let [token (ptok/token-proxy "plugin-id" file-id set-id token-id)]
|
||||
(set! (.-value token) "Inter, Arial")
|
||||
(t/is (= set-id (:set-id @captured)))
|
||||
(t/is (= token-id (:token-id @captured)))
|
||||
(t/is (= ["Inter" "Arial"] (get-in @captured [:attrs :value])))))))
|
||||
|
||||
(t/deftest typography-token-resolved-value-is-plugin-array-shape
|
||||
(let [token (ctob/make-token
|
||||
{:name "type.body"
|
||||
:type :typography
|
||||
:value {:font-family ["Inter" "Arial"]
|
||||
:font-size "16px"
|
||||
:font-weight "600"
|
||||
:line-height "20px"
|
||||
:letter-spacing "1"
|
||||
:text-case "uppercase"
|
||||
:text-decoration "underline"}})
|
||||
result (get-resolved-value token {(:name token) token})
|
||||
entry (aget result 0)]
|
||||
(t/is (array? result))
|
||||
(t/is (= ["Inter" "Arial"] (vec (aget entry "fontFamilies"))))
|
||||
(t/is (= 16 (aget entry "fontSizes")))
|
||||
(t/is (= "600" (aget entry "fontWeights")))
|
||||
(t/is (= 20 (aget entry "lineHeight")))
|
||||
(t/is (= "uppercase" (aget entry "textCase")))
|
||||
(t/is (= "underline" (aget entry "textDecoration")))))
|
||||
|
||||
(t/deftest shadow-token-resolved-value-is-plugin-array-shape
|
||||
(let [token (ctob/make-token
|
||||
{:name "shadow.card"
|
||||
:type :shadow
|
||||
:value [{:offset-x "1px"
|
||||
:offset-y "2px"
|
||||
:blur "3px"
|
||||
:spread "4px"
|
||||
:color "#000000"
|
||||
:inset false}]})
|
||||
result (get-resolved-value token {(:name token) token})
|
||||
entry (aget result 0)]
|
||||
(t/is (array? result))
|
||||
(t/is (= 1 (aget entry "offsetX")))
|
||||
(t/is (= 2 (aget entry "offsetY")))
|
||||
(t/is (= 3 (aget entry "blur")))
|
||||
(t/is (= 4 (aget entry "spread")))))
|
||||
|
||||
(t/deftest font-family-token-resolved-value-is-string-array
|
||||
(let [token (ctob/make-token
|
||||
{:name "font.primary"
|
||||
:type :font-family
|
||||
:value ["Inter" "Arial"]})
|
||||
result (get-resolved-value token {(:name token) token})]
|
||||
(t/is (array? result))
|
||||
(t/is (= ["Inter" "Arial"] (vec result)))))
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
in :fetching) and are permanently stuck with fallback-font layout metrics."
|
||||
(:require
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
@ -108,3 +110,21 @@
|
||||
;; process-pending fires update-text-layouts, it covers shape-b too.
|
||||
(t/is (= 2 (count (:shapes @captured)))
|
||||
"Both shapes are in process-pending so font-load covers all of them")))
|
||||
|
||||
(t/deftest empty-grid-tracks-do-not-allocate-zero-bytes
|
||||
(let [calls (atom [])
|
||||
;; `h/call` is a macro that resolves the wasm function off the module
|
||||
;; via `unchecked-get`, so it cannot be redefined. Mock the module
|
||||
;; itself with recording stubs and let the real macro expansion run.
|
||||
module #js {"_set_grid_rows" (fn [& _] (swap! calls conj [:call "_set_grid_rows"]) nil)
|
||||
"_set_grid_columns" (fn [& _] (swap! calls conj [:call "_set_grid_columns"]) nil)}]
|
||||
(with-redefs [mem/alloc (fn [size]
|
||||
(swap! calls conj [:alloc size])
|
||||
0)
|
||||
wasm/internal-module module]
|
||||
(wasm.api/set-grid-layout-rows [])
|
||||
(wasm.api/set-grid-layout-columns []))
|
||||
(t/is (not-any? #(= :alloc (first %)) @calls))
|
||||
(t/is (= [[:call "_set_grid_rows"]
|
||||
[:call "_set_grid_columns"]]
|
||||
@calls))))
|
||||
|
||||
@ -27,12 +27,18 @@
|
||||
[frontend-tests.logic.groups-test]
|
||||
[frontend-tests.logic.pasting-in-containers-test]
|
||||
[frontend-tests.main-errors-test]
|
||||
[frontend-tests.plugins.comments-test]
|
||||
[frontend-tests.plugins.context-shapes-test]
|
||||
[frontend-tests.plugins.file-test]
|
||||
[frontend-tests.plugins.format-test]
|
||||
[frontend-tests.plugins.grid-test]
|
||||
[frontend-tests.plugins.interactions-test]
|
||||
[frontend-tests.plugins.library-test]
|
||||
[frontend-tests.plugins.local-storage-test]
|
||||
[frontend-tests.plugins.page-active-validation-test]
|
||||
[frontend-tests.plugins.page-test]
|
||||
[frontend-tests.plugins.parser-test]
|
||||
[frontend-tests.plugins.shape-bugfixes-test]
|
||||
[frontend-tests.plugins.text-test]
|
||||
[frontend-tests.plugins.tokens-test]
|
||||
[frontend-tests.plugins.utils-test]
|
||||
@ -45,7 +51,9 @@
|
||||
[frontend-tests.tokens.style-dictionary-test]
|
||||
[frontend-tests.tokens.token-errors-test]
|
||||
[frontend-tests.tokens.workspace-tokens-remap-test]
|
||||
[frontend-tests.ui.comments-position-modifier-test]
|
||||
[frontend-tests.ui.ds-controls-numeric-input-test]
|
||||
[frontend-tests.ui.measures-menu-props-test]
|
||||
[frontend-tests.util-object-test]
|
||||
[frontend-tests.util-range-tree-test]
|
||||
[frontend-tests.util-simple-math-test]
|
||||
@ -65,7 +73,8 @@
|
||||
(.exit js/process 1)))
|
||||
|
||||
(def test-namespaces
|
||||
['frontend-tests.code-gen-style-test
|
||||
['frontend-tests.basic-shapes-test
|
||||
'frontend-tests.code-gen-style-test
|
||||
'frontend-tests.copy-as-svg-test
|
||||
'frontend-tests.data.nitrate-test
|
||||
'frontend-tests.data.repo-test
|
||||
@ -87,12 +96,18 @@
|
||||
'frontend-tests.logic.groups-test
|
||||
'frontend-tests.logic.pasting-in-containers-test
|
||||
'frontend-tests.main-errors-test
|
||||
'frontend-tests.plugins.comments-test
|
||||
'frontend-tests.plugins.context-shapes-test
|
||||
'frontend-tests.plugins.file-test
|
||||
'frontend-tests.plugins.format-test
|
||||
'frontend-tests.plugins.grid-test
|
||||
'frontend-tests.plugins.interactions-test
|
||||
'frontend-tests.plugins.library-test
|
||||
'frontend-tests.plugins.local-storage-test
|
||||
'frontend-tests.plugins.page-active-validation-test
|
||||
'frontend-tests.plugins.page-test
|
||||
'frontend-tests.plugins.parser-test
|
||||
'frontend-tests.plugins.shape-bugfixes-test
|
||||
'frontend-tests.plugins.text-test
|
||||
'frontend-tests.plugins.tokens-test
|
||||
'frontend-tests.plugins.utils-test
|
||||
@ -105,13 +120,14 @@
|
||||
'frontend-tests.tokens.style-dictionary-test
|
||||
'frontend-tests.tokens.token-errors-test
|
||||
'frontend-tests.tokens.workspace-tokens-remap-test
|
||||
'frontend-tests.ui.comments-position-modifier-test
|
||||
'frontend-tests.ui.ds-controls-numeric-input-test
|
||||
'frontend-tests.ui.measures-menu-props-test
|
||||
'frontend-tests.util-object-test
|
||||
'frontend-tests.util-range-tree-test
|
||||
'frontend-tests.util-simple-math-test
|
||||
'frontend-tests.util-webapi-test
|
||||
'frontend-tests.worker-snap-test
|
||||
'frontend-tests.basic-shapes-test])
|
||||
'frontend-tests.worker-snap-test])
|
||||
|
||||
(assert (every? find-ns-obj test-namespaces)
|
||||
"test-namespaces contains a namespace that isn't required in runner.cljs")
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.ui.comments-position-modifier-test
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- frame
|
||||
[id]
|
||||
(cts/setup-shape {:id id :type :frame :name "Board"
|
||||
:x 100 :y 100 :width 200 :height 150}))
|
||||
|
||||
(defn- close-point?
|
||||
[a b]
|
||||
(and (mth/close? (:x a) (:x b))
|
||||
(mth/close? (:y a) (:y b))))
|
||||
|
||||
(t/deftest frame-pin-transform-move
|
||||
(let [f (frame (uuid/next))
|
||||
mods (ctm/move-modifiers (gpt/point 10 20))
|
||||
m (dwcm/frame-pin-transform f mods nil)]
|
||||
(t/testing "the comment follows the frame translation"
|
||||
(t/is (close-point? (gpt/point 160 170)
|
||||
(gpt/transform (gpt/point 150 150) m))))))
|
||||
|
||||
(t/deftest frame-pin-transform-rotation
|
||||
(let [f (frame (uuid/next))
|
||||
center (gsh/shape->center f)
|
||||
mods (ctm/rotation (ctm/empty) center 90)
|
||||
m (dwcm/frame-pin-transform f mods nil)
|
||||
p (gpt/point 150 150)]
|
||||
(t/testing "the comment rotates around the frame center"
|
||||
(t/is (close-point? (gpt/transform p (ctm/modifiers->transform mods))
|
||||
(gpt/transform p m))))))
|
||||
|
||||
(t/deftest frame-pin-transform-resize
|
||||
(let [f (frame (uuid/next))
|
||||
mods (ctm/resize (ctm/empty) (gpt/point 2 2) (gpt/point 100 100))
|
||||
m (dwcm/frame-pin-transform f mods nil)
|
||||
p (gpt/point 150 150)]
|
||||
(t/testing "the comment keeps its position without scaling"
|
||||
(t/is (close-point? p (gpt/transform p m))))
|
||||
(t/testing "the comment is not scaled along with the frame"
|
||||
(t/is (not (close-point? (gpt/transform p (ctm/modifiers->transform mods))
|
||||
(gpt/transform p m)))))))
|
||||
|
||||
(t/deftest frame-pin-transform-without-transform
|
||||
(let [f (frame (uuid/next))]
|
||||
(t/testing "no active transform yields no matrix"
|
||||
(t/is (nil? (dwcm/frame-pin-transform f nil nil))))))
|
||||
@ -0,0 +1,51 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.ui.measures-menu-props-test
|
||||
(:require
|
||||
[app.main.ui.workspace.sidebar.options.menus.measures :refer [check-measures-menu-props]]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
;; Shared, identical-by-reference props so the comparator only reacts to the
|
||||
;; `values` differences we are testing.
|
||||
(def ^:private ids #js ["id-1"])
|
||||
(def ^:private shape-type :rect)
|
||||
(def ^:private tokens #js {})
|
||||
|
||||
(defn- props
|
||||
[values]
|
||||
#js {"ids" ids "type" shape-type "appliedTokens" tokens "values" values})
|
||||
|
||||
(def ^:private base-values
|
||||
{:width 100
|
||||
:height 200
|
||||
:layout-item-h-sizing :fix
|
||||
:layout-item-v-sizing :fix})
|
||||
|
||||
(t/deftest test-check-measures-menu-props
|
||||
(t/testing "skips re-render when nothing relevant changed"
|
||||
;; Different map instances with identical scalar content must be treated
|
||||
;; as equal (returns true => memoized, no re-render).
|
||||
(t/is (true? (check-measures-menu-props
|
||||
(props base-values)
|
||||
(props (into {} base-values))))))
|
||||
|
||||
(t/testing "re-renders when horizontal sizing changes but width does not"
|
||||
;; Regression test: toggling fix <-> auto without changing the width value
|
||||
;; must force a re-render so the width input enabled/disabled state updates.
|
||||
(t/is (false? (check-measures-menu-props
|
||||
(props base-values)
|
||||
(props (assoc base-values :layout-item-h-sizing :auto))))))
|
||||
|
||||
(t/testing "re-renders when vertical sizing changes but height does not"
|
||||
(t/is (false? (check-measures-menu-props
|
||||
(props base-values)
|
||||
(props (assoc base-values :layout-item-v-sizing :auto))))))
|
||||
|
||||
(t/testing "re-renders when width changes"
|
||||
(t/is (false? (check-measures-menu-props
|
||||
(props base-values)
|
||||
(props (assoc base-values :width 150)))))))
|
||||
@ -15,18 +15,18 @@
|
||||
"test:watch:e2e": "vitest --browser"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.0",
|
||||
"@types/node": "^25.9.2",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@types/node": "^26.0.1",
|
||||
"@vitest/browser": "^4.1.9",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"@vitest/ui": "^4.1.9",
|
||||
"canvas": "^3.2.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"playwright": "1.61.0",
|
||||
"prettier": "^3.8.4",
|
||||
"vite": "^8.0.16",
|
||||
"playwright": "1.61.1",
|
||||
"prettier": "^3.9.4",
|
||||
"vite": "^8.1.1",
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620"
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"version": "1.2.0-RC1",
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -34,11 +34,11 @@
|
||||
"watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.12",
|
||||
"@zip.js/zip.js": "2.8.11",
|
||||
"concurrently": "^9.2.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"@types/node": "^26.0.1",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"concurrently": "^10.0.3",
|
||||
"date-fns": "^4.4.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"source-map-support": "^0.5.21"
|
||||
}
|
||||
}
|
||||
|
||||
300
library/pnpm-lock.yaml
generated
300
library/pnpm-lock.yaml
generated
@ -5,63 +5,63 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
'@zip.js/zip.js@2.8.11':
|
||||
hash: 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95
|
||||
path: patches/@zip.js__zip.js@2.8.11.patch
|
||||
'@zip.js/zip.js@2.8.26': 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.18.12
|
||||
version: 22.19.3
|
||||
specifier: ^26.0.1
|
||||
version: 26.0.1
|
||||
'@zip.js/zip.js':
|
||||
specifier: 2.8.11
|
||||
version: 2.8.11(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)
|
||||
specifier: 2.8.26
|
||||
version: 2.8.26(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)
|
||||
concurrently:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
nodemon:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.11
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
|
||||
packages:
|
||||
|
||||
'@types/node@22.19.3':
|
||||
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
|
||||
'@types/node@26.0.1':
|
||||
resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
|
||||
|
||||
'@zip.js/zip.js@2.8.11':
|
||||
resolution: {integrity: sha512-0fztsk/0ryJ+2PPr9EyXS5/Co7OK8q3zY/xOoozEWaUsL5x+C0cyZ4YyMuUffOO2Dx/rAdq4JMPqW0VUtm+vzA==}
|
||||
'@zip.js/zip.js@2.8.26':
|
||||
resolution: {integrity: sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==}
|
||||
engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
brace-expansion@5.0.7:
|
||||
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@ -70,35 +70,25 @@ packages:
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
chalk@5.6.2:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
cliui@9.0.1:
|
||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
concurrently@9.2.1:
|
||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||
engines: {node: '>=18'}
|
||||
concurrently@10.0.3:
|
||||
resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
date-fns@4.4.0:
|
||||
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
@ -109,8 +99,8 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
emoji-regex@10.6.0:
|
||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
@ -129,6 +119,10 @@ packages:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-east-asian-width@1.6.0:
|
||||
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -137,10 +131,6 @@ packages:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ignore-by-default@1.0.1:
|
||||
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||
|
||||
@ -152,10 +142,6 @@ packages:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -164,14 +150,15 @@ packages:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nodemon@3.1.11:
|
||||
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
|
||||
nodemon@3.1.14:
|
||||
resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -179,8 +166,8 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
picomatch@2.3.2:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
@ -190,20 +177,16 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
rxjs@7.8.2:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
shell-quote@1.8.3:
|
||||
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
|
||||
shell-quote@1.8.4:
|
||||
resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
@ -217,26 +200,22 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
string-width@7.2.0:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
strip-ansi@7.2.0:
|
||||
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@ -255,52 +234,49 @@ packages:
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
undici-types@8.3.0:
|
||||
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
wrap-ansi@9.0.2:
|
||||
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
yargs-parser@22.0.0:
|
||||
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
yargs@18.0.0:
|
||||
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@types/node@22.19.3':
|
||||
'@types/node@26.0.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
undici-types: 8.3.0
|
||||
|
||||
'@zip.js/zip.js@2.8.11(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {}
|
||||
'@zip.js/zip.js@2.8.26(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
picomatch: 2.3.2
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
brace-expansion@5.0.7:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
balanced-match: 4.0.4
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
@ -308,10 +284,7 @@ snapshots:
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
chalk@5.6.2: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
@ -325,30 +298,22 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
cliui@8.0.1:
|
||||
cliui@9.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.2.0
|
||||
wrap-ansi: 9.0.2
|
||||
|
||||
color-convert@2.0.1:
|
||||
concurrently@10.0.3:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
concurrently@9.2.1:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
chalk: 5.6.2
|
||||
rxjs: 7.8.2
|
||||
shell-quote: 1.8.3
|
||||
supports-color: 8.1.1
|
||||
shell-quote: 1.8.4
|
||||
supports-color: 10.2.2
|
||||
tree-kill: 1.2.2
|
||||
yargs: 17.7.2
|
||||
yargs: 18.0.0
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
date-fns@4.4.0: {}
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
@ -356,7 +321,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
supports-color: 5.5.0
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
emoji-regex@10.6.0: {}
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
@ -369,14 +334,14 @@ snapshots:
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-east-asian-width@1.6.0: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
has-flag@3.0.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
ignore-by-default@1.0.1: {}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
@ -385,28 +350,26 @@ snapshots:
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
brace-expansion: 5.0.7
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nodemon@3.1.11:
|
||||
nodemon@3.1.14:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
minimatch: 10.2.5
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.7.3
|
||||
semver: 7.8.5
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.1
|
||||
@ -414,27 +377,25 @@ snapshots:
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
picomatch: 2.3.2
|
||||
|
||||
rxjs@7.8.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
semver@7.7.3: {}
|
||||
semver@7.8.5: {}
|
||||
|
||||
shell-quote@1.8.3: {}
|
||||
shell-quote@1.8.4: {}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
semver: 7.8.5
|
||||
|
||||
source-map-support@0.5.21:
|
||||
dependencies:
|
||||
@ -443,28 +404,22 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
string-width@7.2.0:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
emoji-regex: 10.6.0
|
||||
get-east-asian-width: 1.6.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
strip-ansi@7.2.0:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
supports-color@5.5.0:
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-color@8.1.1:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@ -477,24 +432,23 @@ snapshots:
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
undici-types@8.3.0: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
wrap-ansi@9.0.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
yargs-parser@22.0.0: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
yargs@18.0.0:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
cliui: 9.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
string-width: 7.2.0
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
yargs-parser: 22.0.0
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
patchedDependencies:
|
||||
'@zip.js/zip.js@2.8.11': patches/@zip.js__zip.js@2.8.11.patch
|
||||
'@zip.js/zip.js@2.8.26': patches/@zip.js__zip.js@2.8.11.patch
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot.git"
|
||||
},
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"devDependencies": {
|
||||
"concurrently": "^10.0.3",
|
||||
"prettier": "^3.8.4"
|
||||
"prettier": "^3.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "Shared type definitions and interfaces for Penpot MCP",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"scripts": {
|
||||
"build": "tsc --build --clean && tsc --build",
|
||||
"watch": "tsc --watch",
|
||||
@ -12,7 +12,7 @@
|
||||
"clean": "rm -rf dist/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
|
||||
@ -12,13 +12,13 @@
|
||||
"clean": "rm -rf dist/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@penpot/plugin-styles": "1.4.1",
|
||||
"@penpot/plugin-types": "1.4.1"
|
||||
"@penpot/plugin-styles": "1.4.2",
|
||||
"@penpot/plugin-types": "1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.8",
|
||||
"vite-live-preview": "^0.3.2"
|
||||
"cross-env": "^10.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.0",
|
||||
"vite-live-preview": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,35 +24,35 @@
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"class-validator": "^0.15.1",
|
||||
"express": "^5.1.0",
|
||||
"ioredis": "^5.6.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"ioredis": "^5.11.1",
|
||||
"js-yaml": "^5.2.0",
|
||||
"nrepl-client": "^0.3.0",
|
||||
"penpot-mcp": "file:..",
|
||||
"pino": "^9.10.0",
|
||||
"pino-loki": "^2.6.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-loki": "^3.0.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^4.3.6"
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sharp": "^0.35.2",
|
||||
"ws": "^8.21.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@penpot/mcp-common": "workspace:../common",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/ws": "^8.5.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
|
||||
2154
mcp/pnpm-lock.yaml
generated
2154
mcp/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
@ -16,10 +16,10 @@
|
||||
"fmt": "./scripts/fmt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/node": "^26.0.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"mdts": "^0.20.3",
|
||||
"nrepl-client": "^0.3.0",
|
||||
"opencode-ai": "^1.17.9"
|
||||
"opencode-ai": "^1.17.11"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,36 @@
|
||||
## 1.5.0 (Unreleased)
|
||||
|
||||
### 💣 Breaking changes & Deprecations
|
||||
|
||||
- **plugins-runtime**: changes outside the current page now raise a validation error when the target belongs to a page that is not currently active, instead of silently operating on the active page.
|
||||
- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`.
|
||||
- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items
|
||||
- **plugin-types**: Change return type of `combineAsVariants`
|
||||
- **plugin-types:** Deprecate the legacy `Image` shape interface — image shapes exist only for backward compatibility with old files; new images are embedded in a `Fill` via its `fillImage` (an `ImageData`).
|
||||
- We've solved several inconsistencies accross the API, if you relied on an undocumented property or method be aware that might have changed.
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- **plugins-runtime**: Added `version` field that returns the current version
|
||||
- **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible)
|
||||
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
|
||||
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
|
||||
- **plugin-types**: `penpot.openPage()` now returns `Promise<void>` and should be awaited before performing operations on the new page
|
||||
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
|
||||
- **plugin-types:** Change `LibraryComponent.isVariant()` return type to type guard `this is LibraryVariantComponent`
|
||||
- **plugin-types**: Added `createVariantFromComponents`
|
||||
- **plugin-types**: Change return type of `combineAsVariants`
|
||||
- **plugin-types**: Added `textBounds` property for text shapes
|
||||
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
|
||||
- **plugin-types**: Fix missing `webp` export format in `Export.type`
|
||||
- **plugin-types**: Added `fixedWhenScrolling` property for shapes
|
||||
- **plugin-runtime:** `addToken` now resolves references against all token sets, allowing references to tokens in inactive sets
|
||||
- **plugin-types:** `TokenCatalog.addSet` now accepts an optional `active` flag to create an already-active set (sets are inactive by default)
|
||||
- **plugin-runtime:** A `fontFamilies` token's `resolvedValue` now returns the documented `string[]` (the resolved family list) instead of leaking the raw tokenscript list symbol
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`.
|
||||
- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items
|
||||
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
|
||||
- **plugin-types**: Rename `LibraryTypography.fontFamilies` to `fontFamily` to match the runtime (it holds a single font family, not an array)
|
||||
- **plugin-runtime:** Setting a `LibraryColor`'s `gradient` or `image` now clears the other color representations (solid/gradient/image are mutually exclusive), so the result is a valid color instead of being rejected with "expected valid color"
|
||||
- **plugin-types:** Mark members that have no runtime setter as `readonly`, fixing a mismatch where they were typed as writable: font metadata (`Font.*`, `FontVariant.*`, `FontsContext.all`), the `Ellipse`/`Image`/`SvgRaw` `type` discriminants (now consistent with the other shapes), `File.name`/`pages`/`revn`, `Page.root`, `TokenTheme.activeSets`, `Variants.properties`, `ImageData.*`, the board guide value objects (`GuideColumn`/`GuideRow`/`GuideSquare` and their params — `board.guides` returns a formatted snapshot, so reconfiguring means reassigning the whole array), the `Point` and `Bounds` value objects, the `Penpot.ui`/`Penpot.utils` subcontexts, the derived `Boolean` path data (`d`/`content`/`commands` are computed from the operands; `Boolean` is not editable like a `Path`), and the `EventsMap` event entries (a type-only event→callback map, never assigned). Members that do expose a setter stay writable: `Board.children`, `Path.d`/`content`/`commands` and `FileVersion.label`.
|
||||
|
||||
## 1.4.2 (2026-01-21)
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/contrast-plugin",
|
||||
@ -62,7 +62,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": { "buildTarget": "contrast-plugin:build:production" },
|
||||
"development": {
|
||||
@ -82,7 +82,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/icons-plugin",
|
||||
@ -134,7 +134,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": { "buildTarget": "icons-plugin:build:production" },
|
||||
"development": {
|
||||
@ -154,7 +154,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/lorem-ipsum-plugin",
|
||||
@ -206,7 +206,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "lorem-ipsum-plugin:build:production"
|
||||
@ -228,7 +228,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/table-plugin",
|
||||
@ -280,7 +280,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": { "buildTarget": "table-plugin:build:production" },
|
||||
"development": {
|
||||
@ -300,7 +300,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/rename-layers-plugin",
|
||||
@ -352,7 +352,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "rename-layers-plugin:build:production"
|
||||
@ -374,7 +374,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/colors-to-tokens-plugin",
|
||||
@ -426,7 +426,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "colors-to-tokens-plugin:build:production"
|
||||
@ -448,7 +448,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/poc-state-plugin",
|
||||
@ -499,7 +499,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "poc-state-plugin:build:production"
|
||||
@ -521,7 +521,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/apps/poc-tokens-plugin",
|
||||
@ -573,7 +573,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "poc-tokens-plugin:build:production"
|
||||
|
||||
@ -35,9 +35,7 @@ export interface ResizePluginUIEvent {
|
||||
}
|
||||
|
||||
export type PluginUIEvent =
|
||||
| GETColorsPluginUIEvent
|
||||
| ResizePluginUIEvent
|
||||
| ResetPluginUIEvent;
|
||||
GETColorsPluginUIEvent | ResizePluginUIEvent | ResetPluginUIEvent;
|
||||
|
||||
export interface ThemePluginEvent {
|
||||
type: 'theme';
|
||||
|
||||
@ -24,6 +24,4 @@ export interface ThemePluginEvent {
|
||||
}
|
||||
|
||||
export type PluginMessageEvent =
|
||||
| InitPluginEvent
|
||||
| SelectionPluginEvent
|
||||
| ThemePluginEvent;
|
||||
InitPluginEvent | SelectionPluginEvent | ThemePluginEvent;
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
export type GenerationTypes =
|
||||
| 'paragraphs'
|
||||
| 'sentences'
|
||||
| 'words'
|
||||
| 'characters';
|
||||
'paragraphs' | 'sentences' | 'words' | 'characters';
|
||||
|
||||
export interface InitPluginUIEvent {
|
||||
type: 'ready';
|
||||
@ -35,6 +32,4 @@ export interface ThemePluginEvent {
|
||||
}
|
||||
|
||||
export type PluginMessageEvent =
|
||||
| InitPluginEvent
|
||||
| SelectionPluginEvent
|
||||
| ThemePluginEvent;
|
||||
InitPluginEvent | SelectionPluginEvent | ThemePluginEvent;
|
||||
|
||||
391
plugins/apps/plugin-api-test-suite/README.md
Normal file
391
plugins/apps/plugin-api-test-suite/README.md
Normal file
@ -0,0 +1,391 @@
|
||||
# Plugin API Test Suite
|
||||
|
||||
A Penpot plugin that is a launcher + runner for a battery of tests exercising the
|
||||
Penpot **Plugin API** against a live Penpot instance. It doubles as living
|
||||
documentation of what the public API actually does at runtime.
|
||||
|
||||
- A plain TypeScript + Vite Penpot plugin living in `plugins/apps/plugin-api-test-suite`.
|
||||
- The UI (an iframe) lists auto-discovered tests and lets you run all / a subset /
|
||||
one. Each test shows green (pass) or red (fail, with the error message).
|
||||
- It reports **API coverage**: which members of the public Plugin API the tests
|
||||
exercised, measured against `libs/plugin-types/index.d.ts`.
|
||||
- The same test files run both in the plugin UI and in a headless CI runner, so a
|
||||
test is never written twice.
|
||||
|
||||
This document is the context a developer (or agent) needs to add tests. Read it
|
||||
fully before writing any test.
|
||||
|
||||
## The one rule that matters most
|
||||
|
||||
> **Always call the API through `ctx.penpot`, never the global `penpot`.**
|
||||
|
||||
`ctx.penpot` is a recording proxy. Calls made through it are what count towards
|
||||
coverage and are correctly attributed to the right interface. Calls on the global
|
||||
`penpot` still work but are invisible to coverage. Same for shapes: operate on the
|
||||
objects returned by `ctx.penpot.*` (and on `ctx.board`), not on objects obtained
|
||||
some other way.
|
||||
|
||||
## Running and iterating
|
||||
|
||||
From `plugins/`:
|
||||
|
||||
- Dev server: `pnpm run start:plugin:api-test-suite` (serves on port 4202).
|
||||
- In Penpot: open the Plugin Manager (Ctrl+Alt+P) and install
|
||||
`http://localhost:4202/manifest.json`.
|
||||
- **Hot-reloading tests:** after editing a `*.test.ts`, click **Reload** in the
|
||||
plugin UI. It fetches the freshly built test bundle and swaps in your changes —
|
||||
no need to close/reopen the plugin. (The dev server rebuilds the bundle on save.)
|
||||
- **Adding a _new_ test file:** tests are discovered via `import.meta.glob` at
|
||||
build time, and `vite build --watch` does not reliably pick up a brand-new file
|
||||
(only edits to files already in its graph). After creating a new `*.test.ts`,
|
||||
**restart the watch process** (`pnpm run watch` or `pnpm run init`) and then
|
||||
click **Reload** (or reopen the plugin). Editing an existing test file does not
|
||||
need this.
|
||||
- The UI: tests are shown in **collapsible groups** (from `describe`) with per-group
|
||||
passed/failed/total counts. Run with **Run all**, **Run selected** (per-test or
|
||||
per-group checkboxes), the per-group **Run group**, or the per-row **Run** button.
|
||||
Failures expand to show the error. The coverage panel shows the percentage, a
|
||||
progress bar, and per-interface get/set/call targets.
|
||||
|
||||
## Running in CI
|
||||
|
||||
A headless runner executes the same tests against a live instance via Playwright:
|
||||
|
||||
```
|
||||
E2E_LOGIN_EMAIL=… E2E_LOGIN_PASSWORD=… \
|
||||
pnpm --filter plugin-api-test-suite run test:ci
|
||||
```
|
||||
|
||||
- It builds `headless.js`, logs in, creates a scratch file, injects the test
|
||||
bundle, and prints per-test results + the coverage report.
|
||||
- Exit code is non-zero iff any test failed (coverage does not affect it).
|
||||
- Optional env: `PENPOT_BASE_URL` (default `https://localhost:3449`). Against a
|
||||
local devenv with a self-signed certificate, prefix the command with
|
||||
`NODE_TLS_REJECT_UNAUTHORIZED=0` to avoid a `fetch failed` TLS error.
|
||||
- `PRINT_UNCOVERED=1` dumps the uncovered targets per interface; `PRINT_STATIC=1`
|
||||
dumps the statically-covered ones (see [Coverage](#how-coverage-works-and-how-to-write-tests-that-move-it)).
|
||||
|
||||
CI entry points reuse the exact same test files (`src/ci/headless.ts` discovers
|
||||
them the same way the plugin does).
|
||||
|
||||
### Mocked-backend mode
|
||||
|
||||
The same runner can run without a live instance — it serves the prebuilt
|
||||
frontend via the frontend e2e static server and intercepts every backend RPC
|
||||
with Playwright `page.route`, reusing the frontend e2e mock fixtures:
|
||||
|
||||
```
|
||||
pnpm --filter plugin-api-test-suite run test:ci:mocked
|
||||
```
|
||||
|
||||
(equivalently `MOCK_BACKEND=1 … run test:ci`). No login or backend is needed.
|
||||
This validates the frontend Plugin API binding + in-memory store only, so it
|
||||
can't faithfully reproduce results that depend on real backend behaviour
|
||||
(validation, persistence, generated ids, …). Tests that need the real backend
|
||||
opt out of this mode by tagging themselves `skipIfMocked`:
|
||||
|
||||
```ts
|
||||
test.skipIfMocked('depends on backend validation', (ctx) => {
|
||||
/* … */
|
||||
});
|
||||
|
||||
// or a whole group:
|
||||
describe.skipIfMocked('Backend-dependent', () => {
|
||||
/* … */
|
||||
});
|
||||
```
|
||||
|
||||
Skipped tests are listed in the runner output. The wiring (fixtures, RPC mocks,
|
||||
WebSocket mock) lives in `ci/run-ci.ts`; mocked-mode fidelity is its main
|
||||
limitation, so prefer the live `test:ci` for anything backend-sensitive.
|
||||
|
||||
## Anatomy of a test
|
||||
|
||||
Tests live in `src/tests/*.test.ts` and are **auto-discovered** (via
|
||||
`import.meta.glob`) — just create a file matching that glob, no registration list
|
||||
to update. A file registers one or more tests by calling `test(name, fn)`.
|
||||
|
||||
```ts
|
||||
import { expect } from '../framework/expect';
|
||||
import { test } from '../framework/registry';
|
||||
|
||||
test('creates a rectangle', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
expect(rect.type).toBe('rectangle');
|
||||
rect.name = 'sample-rect';
|
||||
expect(rect.name).toBe('sample-rect');
|
||||
});
|
||||
```
|
||||
|
||||
### Grouping tests
|
||||
|
||||
Wrap related tests in `describe(groupName, fn)` to group them. In the UI each group
|
||||
is a **collapsible section** showing its own passed / failed / total counts, with a
|
||||
"Run group" button and a select-all checkbox. Tests not inside any `describe` fall
|
||||
into the `General` group.
|
||||
|
||||
```ts
|
||||
import { expect } from '../framework/expect';
|
||||
import { describe, test } from '../framework/registry';
|
||||
|
||||
describe('Shapes', () => {
|
||||
test('creates a rectangle', (ctx) => {
|
||||
/* … */
|
||||
});
|
||||
|
||||
test('creates an ellipse', (ctx) => {
|
||||
/* … */
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
`describe` blocks may be nested in a file. Nested names are **joined into a single
|
||||
group path** with `" / "`, so the group reveals the file/area it lives in — e.g.
|
||||
`describe('Layout', () => describe('Flex', …))` produces the group `Layout / Flex`.
|
||||
Wrap each file's tests in a top-level `describe` named after its area so every
|
||||
group is recognizable. Several files may contribute to the same group path (they
|
||||
merge in the UI). Prefer one clear group per feature area.
|
||||
|
||||
In the UI each group header shows an aggregate **status dot** rolled up from its
|
||||
tests: it turns purple while any test in the group is running, red if any failed,
|
||||
green only once every test passed, and grey until then.
|
||||
|
||||
### The test context (`ctx`)
|
||||
|
||||
`fn` receives a `TestContext` (`src/framework/types.ts`):
|
||||
|
||||
- `ctx.penpot` — the recording proxy over the real `penpot` global. Use it for
|
||||
every API call.
|
||||
- `ctx.board` — a **fresh scratch `Board`** created for this test and
|
||||
**removed automatically afterwards**. Append shapes you create to it
|
||||
(`ctx.board.appendChild(shape)`) so the user's canvas is left clean. Do not rely
|
||||
on it persisting between tests.
|
||||
|
||||
The runner also resets shared state between tests: the selection is cleared and the
|
||||
active page is restored to whatever was active when the run started (both through
|
||||
the raw `penpot`, so they aren't credited toward coverage). A test that changes the
|
||||
active page therefore won't leak into later tests.
|
||||
|
||||
### Sync or async
|
||||
|
||||
`fn` may be `void` or `Promise<void>`; async tests are awaited. Use `async (ctx) =>`
|
||||
and `await` when the API call is asynchronous (e.g. `uploadMediaUrl`,
|
||||
`library.availableLibraries()`, token application — see notes below).
|
||||
|
||||
### Naming
|
||||
|
||||
The test name becomes its id (slugified) and is shown in the UI. Keep names unique
|
||||
and descriptive; duplicates are de-duplicated automatically but that's confusing.
|
||||
|
||||
## Assertions
|
||||
|
||||
Import `expect` from `../framework/expect`. It is a small, dependency-free,
|
||||
jest-like matcher set (it must stay dependency-free — it runs inside the SES
|
||||
sandbox). Available matchers:
|
||||
|
||||
- `toBe(expected)` — `Object.is` equality
|
||||
- `toEqual(expected)` — deep structural equality
|
||||
- `toBeTruthy()` / `toBeFalsy()`
|
||||
- `toBeNull()` / `toBeUndefined()` / `toBeDefined()`
|
||||
- `toContain(item)` — substring or array membership
|
||||
- `toHaveLength(n)`
|
||||
- `toBeGreaterThan(n)` / `toBeLessThan(n)`
|
||||
- `toBeCloseTo(n, numDigits?)` — for floats
|
||||
- `toThrow(expected?)` — `expected` is a substring or `RegExp` matched against the
|
||||
error message; pass a function as the value: `expect(() => …).toThrow('msg')`
|
||||
- `.not` negates any matcher: `expect(x).not.toBeNull()`
|
||||
|
||||
For asynchronous failures use `expectReject(promiseOrThunk, expected?)`: `toThrow`
|
||||
calls its argument synchronously, so it can't catch a rejected promise, whereas
|
||||
`expectReject` awaits and asserts the rejection (string includes / RegExp on the
|
||||
message).
|
||||
|
||||
A failing matcher throws; the runner turns that into a red test with the message.
|
||||
You can also just `throw new Error('…')` to fail a test.
|
||||
|
||||
> Do not add other assertion libraries. Anything imported here is bundled into the
|
||||
> sandbox and must be SES-safe and dependency-free.
|
||||
|
||||
## How coverage works (and how to write tests that move it)
|
||||
|
||||
Coverage is **type-aware** and tracks three separate targets per member:
|
||||
|
||||
- **`name (get)`** — reading a property (`const n = shape.name`)
|
||||
- **`name (set)`** — writing a property (`shape.name = 'x'`)
|
||||
- **`appendChild()`** — calling a method (credited only when actually **called**,
|
||||
not when merely referenced)
|
||||
|
||||
Implications when writing tests:
|
||||
|
||||
- A property has independent get/set targets. To cover both, read it _and_ write
|
||||
it. Read-only properties (declared `readonly` in the d.ts) only have a get
|
||||
target; methods only have a call target.
|
||||
- Accessing a member through a value you got from `ctx.penpot` is what counts.
|
||||
Reaching a nested object also counts: e.g. `ctx.board.children[0].type` records
|
||||
`Board.children (get)` and then the element's `type` get, resolved to the
|
||||
concrete shape type at runtime.
|
||||
- Coverage **accumulates across a run**. Running all tests aggregates every test's
|
||||
accesses. Running a single test shows only that test's accesses.
|
||||
|
||||
### Recorded vs. effective coverage
|
||||
|
||||
The report distinguishes three states per target:
|
||||
|
||||
- **Covered (recorded)** — credited by the recording proxy (green).
|
||||
- **Statically covered** — exercised behaviourally by the tests but the proxy
|
||||
_structurally cannot_ credit it (shown in a distinct colour). These come from a
|
||||
curated allowlist in `src/framework/static-coverage.ts`, keyed by
|
||||
`Interface.member#mode`. See [Coverage notes](#coverage-notes) for which members
|
||||
and why.
|
||||
- **Uncovered** — neither.
|
||||
|
||||
The header shows two numbers: the **recorded** percentage (what the proxy actually
|
||||
credited) and the **effective** percentage (recorded + statically covered).
|
||||
Recorded coverage always wins, so listing a target in the static allowlist that
|
||||
turns out to be recorded is harmless — it simply never shows as static. Coverage is
|
||||
report-only; it never fails a run or the build.
|
||||
|
||||
The denominator comes from `src/generated/api-surface.json`, generated from
|
||||
`libs/plugin-types/index.d.ts`. If the Plugin API types change, regenerate it:
|
||||
|
||||
```
|
||||
pnpm --filter plugin-api-test-suite run gen:api
|
||||
```
|
||||
|
||||
## Runtime details you need to know
|
||||
|
||||
- **Shape `type` values** returned at runtime: `Board` → `'board'`,
|
||||
`Rectangle` → `'rectangle'`, `Ellipse` → `'ellipse'`, plus `'text'`, `'path'`,
|
||||
`'group'`, `'image'`, `'svg-raw'`. (`createRectangle().type === 'rectangle'`.)
|
||||
- `createText(str)` returns `Text | null` — guard the result (`if (text) { … }`).
|
||||
- `width`/`height` are read-only; use `resize(w, h)`. `x`/`y` are writable.
|
||||
- The plugin manifest already requests broad permissions (`content:*`,
|
||||
`library:*`, `user:read`, `comment:*`, `allow:downloads`, `allow:localstorage`),
|
||||
so most of the API is callable from tests without changes.
|
||||
- The runner sets `throwValidationErrors = true` and `naturalChildOrdering = true`,
|
||||
so invalid API usage throws (surfacing as a red test) and `children` is always in
|
||||
z-index order.
|
||||
- The runtime is SES-sandboxed: no Node APIs, no DOM, no extra npm deps inside
|
||||
tests. Stick to the Plugin API, `expect`, and plain JS.
|
||||
|
||||
## Coverage notes
|
||||
|
||||
The suite covers a large majority of the type surface. The remaining members are
|
||||
uncovered or only _statically_ covered for the reasons below — **not** missing
|
||||
tests. Note these notes can drift as the API is fixed: when in doubt, write the
|
||||
test asserting the documented correct behaviour and run `test:ci` to see what
|
||||
actually happens.
|
||||
|
||||
### Exercised behaviourally but not creditable by the recorder (statically covered)
|
||||
|
||||
Listed in `src/framework/static-coverage.ts`:
|
||||
|
||||
- **`ContextTypesUtils.*` and `ContextGeometryUtils.center`** — `penpot.utils.types`
|
||||
and `penpot.utils.geometry` are frozen (SES) data properties, so the recording
|
||||
proxy must return them raw and cannot wrap their members. Both are exercised
|
||||
behaviourally in `platform.test.ts`.
|
||||
- **`ColorShapeInfo.shapesInfo`, `ColorShapeInfoEntry.*`** — `shapesColors()` has an
|
||||
unresolved return type in the generated surface (`type: null`), so the recorder
|
||||
hands the result back raw and can't attribute nested access. Exercised in
|
||||
`colors.test.ts`. (Alternatively, resolving the return type in
|
||||
`tools/gen-api-surface.ts` would make these genuinely recorded.)
|
||||
- **`EventsMap.*`** — a type map, not a runtime object. `on`/`off` are credited on
|
||||
`Penpot`, never as `EventsMap` members. The deterministic events
|
||||
(`selectionchange`, `shapechange`) are exercised in `events.test.ts`.
|
||||
- **`ShapeBase.fills`** — every concrete shape redeclares `fills`, so accesses are
|
||||
attributed to the concrete type (`Rectangle.fills`, …); the base-interface target
|
||||
is never the attribution.
|
||||
- **`LibraryVariantComponent.*`** — the recorder types a component as
|
||||
`LibraryComponent` and can't narrow to `LibraryVariantComponent` via the
|
||||
`isVariant()` type-guard. The behaviour is exercised via `VariantContainer.variants`
|
||||
in `variants.test.ts`.
|
||||
|
||||
### Read-only at runtime
|
||||
|
||||
Members that have no setter in the runtime binding (`frontend/src/app/plugins/*.cljs`)
|
||||
are now marked `readonly` in the Plugin API d.ts (`Font.*`, `FontVariant.*`,
|
||||
`FontsContext.all`, `Image/Ellipse/SvgRaw.type`, `File.name/pages/revn`, `Page.root`,
|
||||
`TokenTheme.activeSets`, `Variants.properties`, `ImageData.*`, and the board guide
|
||||
value objects `GuideColumn/GuideRow/GuideSquare` and their params — `board.guides`
|
||||
returns a formatted snapshot, so guides are reconfigured by reassigning the whole
|
||||
array, not by mutating a returned guide), the `Point`/`Bounds` value objects, the
|
||||
`Penpot.ui`/`Penpot.utils` subcontexts, and the derived `Boolean` path data
|
||||
(`d`/`content`/`commands` are computed from the operands — a `Boolean` isn't editable
|
||||
like a `Path`). They therefore have only a `(get)` target and need no runtime
|
||||
assertion — the type system enforces the contract.
|
||||
|
||||
Members that **do** have a runtime setter stay writable, even when the setter
|
||||
rejects some inputs (that's input validation, not read-only-ness): `Board.children`
|
||||
(assigning a reordered array reorders the children), `Path.d/content/commands`
|
||||
(editing the path), and `FileVersion.label` (relabels the version).
|
||||
|
||||
### Excluded from coverage
|
||||
|
||||
`tools/gen-api-surface.ts` drops two categories from the denominator so they never
|
||||
count:
|
||||
|
||||
- **`@deprecated` interfaces and members** — the legacy `Image` shape interface
|
||||
(images live in a `Fill` via `fillImage`), `Color.refId`/`refFile`, and the
|
||||
`Boolean`/`Path` `toD()`/`content` path accessors.
|
||||
- **Members removed by the public interface via `Omit`** — `Context` is the
|
||||
internal interface and the public `Penpot` is `Omit<Context, 'addListener' |
|
||||
'removeListener'>` (those are superseded by `on`/`off`). The generator honors the
|
||||
`Omit`, so `Context.addListener`/`removeListener` aren't reachable surface and
|
||||
don't count.
|
||||
|
||||
### Red tests pinning confirmed API bugs
|
||||
|
||||
When a member is confirmed broken, add a test that asserts its **correct** behaviour
|
||||
and comment it as blocked-by-bug; it stays red until the API is fixed and then turns
|
||||
green (at which point drop the "API bug" framing). There are currently no such red
|
||||
tests — e.g. the `fontFamilies` token `resolvedValue` bug (it used to leak the raw
|
||||
tokenscript structure instead of `string[]`) has since been fixed.
|
||||
|
||||
### d.ts / runtime mismatches
|
||||
|
||||
`strokeStyle: 'none'` is listed in the d.ts but rejected at runtime ("Value not
|
||||
valid"); `fills-strokes.test.ts` pins this with a `toThrow`.
|
||||
|
||||
### External state / not reachable headless
|
||||
|
||||
- **`ActiveUser.position/zoom`** — needs a second collaborator in the file.
|
||||
- **`LibrarySummary.*`, `LibraryContext.connectLibrary`** — need a published shared
|
||||
library.
|
||||
- **`FileVersion.restore`, `Penpot.closePlugin`, `Penpot.ui`, `Context.openViewer`** —
|
||||
tear down or navigate away from the running plugin/workspace.
|
||||
- **`FileVersion.pin`** — only converts a _system_ autosave to a permanent version;
|
||||
a plugin can only create manual versions (`saveVersion`), so `pin()` always
|
||||
rejects.
|
||||
- **`Context.addListener/removeListener`** — omitted from the `penpot` global
|
||||
(`Omit<Context, 'addListener' | 'removeListener'>`), so unreachable via `penpot`.
|
||||
- **`EventsMap` events `pagechange/filechange/themechange/contentsave/finish`** —
|
||||
can't be triggered deterministically in the headless runner.
|
||||
|
||||
## Checklist before finishing
|
||||
|
||||
- [ ] Test file is `src/tests/<name>.test.ts` and uses `test(...)` + `expect`,
|
||||
ideally wrapped in a `describe('<Group>', …)`.
|
||||
- [ ] All API calls go through `ctx.penpot`; shapes are appended to `ctx.board`.
|
||||
- [ ] Created shapes don't leak (rely on the scratch board cleanup; don't touch the
|
||||
user's existing content).
|
||||
- [ ] Lint/format/typecheck pass:
|
||||
`pnpm --filter plugin-api-test-suite run lint` and, from `plugins/`,
|
||||
`pnpm exec prettier --check "apps/plugin-api-test-suite/**/*.{ts,css,json}"`.
|
||||
- [ ] If you relied on new API members, `gen:api` was re-run so coverage reflects
|
||||
them.
|
||||
|
||||
## Where things live (for deeper changes)
|
||||
|
||||
- `src/framework/registry.ts` — `test()`, `describe()`, `getTests()`, `setTests()` (reload).
|
||||
- `src/framework/runner.ts` — runs tests, scratch board lifecycle, per-test state reset, coverage.
|
||||
- `src/framework/coverage.ts` — the recording proxy + coverage computation.
|
||||
- `src/framework/static-coverage.ts` — the statically-covered allowlist.
|
||||
- `src/framework/expect.ts` — the assertion library.
|
||||
- `src/framework/types.ts` — `TestContext`, `TestResult`, `CoverageReport`, etc.
|
||||
- `tools/gen-api-surface.ts` — generates `src/generated/api-surface.json`.
|
||||
- `src/plugin.ts` (sandbox), `src/ui.ts` (iframe), `src/model.ts` (messages).
|
||||
- `src/ci/headless.ts` + `ci/run-ci.ts` — CI path.
|
||||
|
||||
Writing tests should only ever require touching `src/tests/`.
|
||||
60
plugins/apps/plugin-api-test-suite/ci/fixtures/get-file.json
Normal file
60
plugins/apps/plugin-api-test-suite/ci/fixtures/get-file.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"fdata/shape-data-type",
|
||||
"fdata/path-data",
|
||||
"components/v2",
|
||||
"design-tokens/v1",
|
||||
"variants/v1",
|
||||
"plugins/runtime"
|
||||
]
|
||||
},
|
||||
"~: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": ["~u66697432-c33d-8055-8006-2c62cc084cad"],
|
||||
"~:pages-index": {
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad": {
|
||||
"~#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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
475
plugins/apps/plugin-api-test-suite/ci/run-ci.ts
Normal file
475
plugins/apps/plugin-api-test-suite/ci/run-ci.ts
Normal file
@ -0,0 +1,475 @@
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import type { CoverageReport, TestResult } from '../src/framework/types';
|
||||
|
||||
// Out-of-sandbox CI driver (Node + Playwright). Injects the prebuilt
|
||||
// `headless.js` bundle (built from the in-sandbox entry `src/ci/headless.ts` —
|
||||
// note: a different `ci/` directory) into the plugin sandbox via
|
||||
// `globalThis.ɵloadPlugin` and captures results/coverage from the page console.
|
||||
// Two modes:
|
||||
//
|
||||
// - LIVE (default): logs into a real Penpot instance (devenv), creates a scratch
|
||||
// file, and drives the real backend + frontend end-to-end.
|
||||
// Required env: E2E_LOGIN_EMAIL, E2E_LOGIN_PASSWORD.
|
||||
// Optional env: PENPOT_BASE_URL (default https://localhost:3449).
|
||||
//
|
||||
// - MOCKED (`MOCK_BACKEND=1`): serves the prebuilt frontend bundle via the e2e
|
||||
// static server and intercepts every backend RPC with Playwright `page.route`,
|
||||
// reusing the frontend e2e mock fixtures. No backend/login needed. Validates
|
||||
// the frontend Plugin API binding + in-memory store only; results that depend
|
||||
// on real backend behaviour are not faithfully reproduced, so those tests are
|
||||
// skipped via the `skipIfMocked` tag.
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
// here = <root>/plugins/apps/plugin-api-test-suite/ci
|
||||
const repoRoot = resolve(here, '../../../../');
|
||||
const frontendDir = resolve(repoRoot, 'frontend');
|
||||
const e2eDataDir = resolve(frontendDir, 'playwright/data');
|
||||
|
||||
const MOCKED = !!process.env['MOCK_BACKEND'];
|
||||
const MOCK_BASE_URL = 'http://localhost:3000';
|
||||
const apiUrl = MOCKED
|
||||
? MOCK_BASE_URL
|
||||
: (process.env['PENPOT_BASE_URL'] ?? 'https://localhost:3449');
|
||||
|
||||
const headlessBundlePath = resolve(
|
||||
here,
|
||||
'../../../dist/apps/plugin-api-test-suite/headless.js',
|
||||
);
|
||||
|
||||
// Source the permissions from the same manifest the real plugin ships with, so
|
||||
// the CI sandbox never drifts from what users actually grant.
|
||||
const manifestPath = resolve(here, '../public/manifest.json');
|
||||
const PERMISSIONS: string[] = (
|
||||
JSON.parse(readFileSync(manifestPath, 'utf-8')) as { permissions: string[] }
|
||||
).permissions;
|
||||
|
||||
function cleanId(id: string): string {
|
||||
return id.replace('~u', '');
|
||||
}
|
||||
|
||||
interface FileRpc {
|
||||
'~:id': string;
|
||||
'~:project-id': string;
|
||||
'~:data': { '~:pages': string[] };
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const email = process.env['E2E_LOGIN_EMAIL'];
|
||||
const password = process.env['E2E_LOGIN_PASSWORD'];
|
||||
if (!email || !password) {
|
||||
throw new Error('E2E_LOGIN_EMAIL / E2E_LOGIN_PASSWORD must be set');
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/main/methods/login-with-password`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
},
|
||||
);
|
||||
|
||||
const loginData = await response.json();
|
||||
const authToken = response.headers
|
||||
.getSetCookie()
|
||||
.find((cookie) => cookie.startsWith('auth-token='))
|
||||
?.split(';')[0];
|
||||
|
||||
if (!authToken)
|
||||
throw new Error('Login failed: no auth-token cookie returned');
|
||||
|
||||
return { authToken, defaultProjectId: loginData['~:default-project-id'] };
|
||||
}
|
||||
|
||||
async function createFile(
|
||||
authToken: string,
|
||||
projectId: string,
|
||||
): Promise<FileRpc> {
|
||||
const response = await fetch(`${apiUrl}/api/main/methods/create-file`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/transit+json',
|
||||
cookie: authToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'~:name': `api-test-suite ${new Date().toISOString()}`,
|
||||
'~:project-id': projectId,
|
||||
'~:features': {
|
||||
'~#set': [
|
||||
'fdata/objects-map',
|
||||
'fdata/pointer-map',
|
||||
'fdata/shape-data-type',
|
||||
'fdata/path-data',
|
||||
'design-tokens/v1',
|
||||
'variants/v1',
|
||||
'components/v2',
|
||||
'styles/v2',
|
||||
'layout/grid',
|
||||
'plugins/runtime',
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return (await response.json()) as FileRpc;
|
||||
}
|
||||
|
||||
function getFileUrl(file: FileRpc): string {
|
||||
const projectId = cleanId(file['~:project-id']);
|
||||
const fileId = cleanId(file['~:id']);
|
||||
const pageId = cleanId(file['~:data']['~:pages'][0]);
|
||||
return `${apiUrl}/#/workspace/${projectId}/${fileId}?page-id=${pageId}`;
|
||||
}
|
||||
|
||||
// --- Mocked mode setup -------------------------------------------------------
|
||||
|
||||
// Ids of the mocked full-feature file fixture (`ci/fixtures/get-file.json`),
|
||||
// kept in sync with the frontend e2e fixtures.
|
||||
const MOCK_TEAM_ID = 'c7ce0794-0992-8105-8004-38e630f7920a';
|
||||
const MOCK_FILE_ID = 'c7ce0794-0992-8105-8004-38f280443849';
|
||||
const MOCK_PAGE_ID = '66697432-c33d-8055-8006-2c62cc084cad';
|
||||
|
||||
// Workspace-load RPCs mirrored from the frontend e2e harness
|
||||
// (WorkspacePage.init + setupEmptyFile). Maps RPC glob -> fixture file relative
|
||||
// to frontend/playwright/data.
|
||||
const MOCK_RPCS: Record<string, string> = {
|
||||
'get-profile': 'logged-in-user/get-profile-logged-in.json',
|
||||
'get-teams': 'get-teams.json',
|
||||
'get-team?id=*': 'workspace/get-team-default.json',
|
||||
'get-team-members?team-id=*':
|
||||
'logged-in-user/get-team-members-your-penpot.json',
|
||||
'get-team-users?file-id=*': 'logged-in-user/get-team-users-single-user.json',
|
||||
'get-project?id=*': 'workspace/get-project-default.json',
|
||||
'get-comment-threads?file-id=*': 'workspace/get-comment-threads-empty.json',
|
||||
'get-profiles-for-file-comments?file-id=*':
|
||||
'workspace/get-profile-for-file-comments.json',
|
||||
'get-file-object-thumbnails?file-id=*':
|
||||
'workspace/get-file-object-thumbnails-blank.json',
|
||||
'get-font-variants?team-id=*': 'workspace/get-font-variants-empty.json',
|
||||
'get-file-fragment?file-id=*': 'workspace/get-file-fragment-blank.json',
|
||||
'get-file-libraries?file-id=*': 'workspace/get-file-libraries-empty.json',
|
||||
'update-profile-props': 'workspace/update-profile-empty.json',
|
||||
};
|
||||
|
||||
// Persistence (`update-file`) response shape the frontend expects: it reads
|
||||
// `revn`/`lagged` (persistence.cljs `update-file-revn`). `revn` is merged with
|
||||
// `max`, so a low value is harmless.
|
||||
const UPDATE_FILE_RESPONSE = JSON.stringify({ '~:revn': 1, '~:lagged': [] });
|
||||
|
||||
async function waitForServer(url: string, timeoutMs = 30000): Promise<void> {
|
||||
const start = Date.now();
|
||||
for (;;) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok || res.status === 404) return; // static server is up
|
||||
} catch {
|
||||
/* not up yet */
|
||||
}
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
throw new Error(`Timed out waiting for server at ${url}`);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
}
|
||||
|
||||
function startE2eServer(): ChildProcess {
|
||||
// Reuse the frontend e2e static server: it serves frontend/resources/public
|
||||
// on port 3000, which is also the host the app opens its notifications
|
||||
// WebSocket against (ws://localhost:3000/ws/notifications) — so the WS mock
|
||||
// below matches without extra config.
|
||||
const child = spawn('node', ['scripts/e2e-server.js'], {
|
||||
cwd: frontendDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
// Install the frontend e2e WebSocket mock so the workspace's notifications
|
||||
// socket can be "opened" without a backend.
|
||||
async function installWebSocketMock(page: Page): Promise<void> {
|
||||
const created = new Set<string>();
|
||||
await page.exposeFunction('onMockWebSocketConstructor', (url: string) => {
|
||||
created.add(url);
|
||||
});
|
||||
await page.addInitScript({
|
||||
path: resolve(frontendDir, 'playwright/scripts/MockWebSocket.js'),
|
||||
});
|
||||
// Stash the helper on the page object for later use.
|
||||
(page as unknown as { __wsCreated: Set<string> }).__wsCreated = created;
|
||||
}
|
||||
|
||||
async function openNotificationsWebSocket(page: Page): Promise<void> {
|
||||
const created = (page as unknown as { __wsCreated: Set<string> }).__wsCreated;
|
||||
const start = Date.now();
|
||||
let wsUrl: string | undefined;
|
||||
while (!wsUrl) {
|
||||
wsUrl = [...created].find((u) => u.includes('ws/notifications'));
|
||||
if (wsUrl) break;
|
||||
if (Date.now() - start > 30000) {
|
||||
throw new Error('Timed out waiting for notifications WebSocket');
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
await page.evaluate((url) => {
|
||||
(
|
||||
WebSocket as unknown as {
|
||||
getByURL: (u: string) => { mockOpen: () => void } | undefined;
|
||||
}
|
||||
)
|
||||
.getByURL(url)
|
||||
?.mockOpen();
|
||||
}, wsUrl);
|
||||
}
|
||||
|
||||
async function setupMockedRoutes(page: Page): Promise<void> {
|
||||
// Config flags: deterministic empty flags (mirror BasePage.mockConfigFlags).
|
||||
await page.route('**/js/config.js*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/javascript',
|
||||
body: 'var penpotFlags = "";\n',
|
||||
}),
|
||||
);
|
||||
|
||||
// Workspace-load RPCs from fixtures.
|
||||
for (const [rpc, fixture] of Object.entries(MOCK_RPCS)) {
|
||||
await page.route(`**/api/main/methods/${rpc}`, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/transit+json',
|
||||
path: resolve(e2eDataDir, fixture),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// get-file: the custom full-feature fixture (enables plugins/runtime,
|
||||
// design-tokens/v1, variants/v1, ...). Without these features active the
|
||||
// plugin runtime never initialises.
|
||||
await page.route(/\/api\/main\/methods\/get-file\?/, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/transit+json',
|
||||
path: resolve(here, 'fixtures/get-file.json'),
|
||||
}),
|
||||
);
|
||||
|
||||
// Blanket no-op persistence: most of the Plugin API mutates the in-memory
|
||||
// store optimistically, so a 200 `update-file` mock is enough for the bulk of
|
||||
// the suite to run against in-memory state.
|
||||
await page.route(/\/api\/main\/methods\/update-file\b/, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/transit+json',
|
||||
body: UPDATE_FILE_RESPONSE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function mockedFileUrl(): string {
|
||||
return `${MOCK_BASE_URL}/#/workspace?team-id=${MOCK_TEAM_ID}&file-id=${MOCK_FILE_ID}&page-id=${MOCK_PAGE_ID}`;
|
||||
}
|
||||
|
||||
// --- Reporting ---------------------------------------------------------------
|
||||
|
||||
function printReport(
|
||||
results: TestResult[],
|
||||
coverage: CoverageReport | null,
|
||||
skipped: string[],
|
||||
) {
|
||||
// Each result is already printed live as it streams in; here we only recap the
|
||||
// failures so they're easy to find at the bottom of a long run.
|
||||
const failures = results.filter((r) => r.status === 'fail');
|
||||
if (failures.length > 0) {
|
||||
console.log('\nFailures:');
|
||||
for (const r of failures) {
|
||||
console.log(` ✗ ${r.name} (${r.durationMs}ms)`);
|
||||
if (r.error) {
|
||||
console.log(` ${r.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skipped.length > 0) {
|
||||
console.log(`\nSkipped (mocked mode): ${skipped.length}`);
|
||||
for (const name of skipped) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (coverage) {
|
||||
console.log(
|
||||
`\nAPI coverage (report-only): ${coverage.percent}% recorded ` +
|
||||
`(${coverage.covered}/${coverage.total}), ` +
|
||||
`${coverage.effectivePercent}% effective ` +
|
||||
`(+${coverage.staticallyCovered} statically covered)`,
|
||||
);
|
||||
|
||||
// Opt-in dump of the uncovered targets per interface, to drive test writing.
|
||||
if (process.env['PRINT_UNCOVERED']) {
|
||||
console.log('\nUncovered targets by interface:');
|
||||
for (const [iface, info] of Object.entries(coverage.byInterface)) {
|
||||
if (info.uncovered.length > 0) {
|
||||
console.log(` ${iface}: ${info.uncovered.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opt-in dump of the statically-covered targets (exercised behaviourally but
|
||||
// not creditable through the recording proxy).
|
||||
if (process.env['PRINT_STATIC']) {
|
||||
console.log('\nStatically covered targets by interface:');
|
||||
for (const [iface, info] of Object.entries(coverage.byInterface)) {
|
||||
if (info.staticallyCovered.length > 0) {
|
||||
console.log(` ${iface}: ${info.staticallyCovered.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const bundle = readFileSync(headlessBundlePath, 'utf-8');
|
||||
|
||||
let server: ChildProcess | undefined;
|
||||
let fileUrl: string;
|
||||
let authToken: string | undefined;
|
||||
|
||||
if (MOCKED) {
|
||||
server = startE2eServer();
|
||||
await waitForServer(MOCK_BASE_URL);
|
||||
fileUrl = mockedFileUrl();
|
||||
} else {
|
||||
const session = await login();
|
||||
authToken = session.authToken;
|
||||
const file = await createFile(authToken, session.defaultProjectId);
|
||||
fileUrl = getFileUrl(file);
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
args: ['--ignore-certificate-errors'],
|
||||
});
|
||||
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
if (authToken) {
|
||||
await context.addCookies([
|
||||
{ name: 'auth-token', value: authToken.split('=')[1], url: apiUrl },
|
||||
]);
|
||||
}
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
if (MOCKED) {
|
||||
await installWebSocketMock(page);
|
||||
await setupMockedRoutes(page);
|
||||
}
|
||||
|
||||
// The bundle runs inside an SES Compartment (its own `globalThis`), so a page
|
||||
// `addInitScript` global can't reach it. Prepend the mocked flag straight into
|
||||
// the evaluated code so the bundle's `runTests` excludes `skipIfMocked` tests.
|
||||
const injectedCode = MOCKED
|
||||
? `globalThis.__PLUGIN_SUITE_MOCKED__ = true;\n${bundle}`
|
||||
: bundle;
|
||||
|
||||
const results: TestResult[] = [];
|
||||
let coverage: CoverageReport | null = null;
|
||||
let skipped: string[] = [];
|
||||
let fatal: string | null = null;
|
||||
|
||||
console.log('\nRunning tests:');
|
||||
const done = new Promise<void>((resolvePromise) => {
|
||||
page.on('console', (msg) => {
|
||||
const text = msg.text();
|
||||
if (text.startsWith('__TEST_RESULT__ ')) {
|
||||
const result: TestResult = JSON.parse(
|
||||
text.slice('__TEST_RESULT__ '.length),
|
||||
);
|
||||
results.push(result);
|
||||
// Print each result as it streams in so the run shows live progress
|
||||
// instead of staying silent until it finishes.
|
||||
const icon = result.status === 'pass' ? '✓' : '✗';
|
||||
console.log(` ${icon} ${result.name} (${result.durationMs}ms)`);
|
||||
if (result.status === 'fail' && result.error) {
|
||||
console.log(` ${result.error}`);
|
||||
}
|
||||
} else if (text.startsWith('__TEST_COVERAGE__ ')) {
|
||||
coverage = JSON.parse(text.slice('__TEST_COVERAGE__ '.length));
|
||||
} else if (text.startsWith('__TEST_SKIPPED__ ')) {
|
||||
skipped = JSON.parse(text.slice('__TEST_SKIPPED__ '.length));
|
||||
} else if (text.startsWith('__TEST_DONE__ ')) {
|
||||
resolvePromise();
|
||||
} else if (text.startsWith('__TEST_FATAL__ ')) {
|
||||
fatal = JSON.parse(text.slice('__TEST_FATAL__ '.length)).message;
|
||||
resolvePromise();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(fileUrl);
|
||||
|
||||
if (MOCKED) {
|
||||
await openNotificationsWebSocket(page);
|
||||
}
|
||||
|
||||
await page.waitForSelector('[data-testid="viewport"]');
|
||||
// The plugin runtime initialises asynchronously after the file's features are
|
||||
// active; wait for the loader to be exposed before injecting the bundle.
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (globalThis as unknown as { ɵloadPlugin?: unknown })
|
||||
.ɵloadPlugin === 'function',
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
await page.evaluate(
|
||||
({ code, permissions }) => {
|
||||
(
|
||||
globalThis as unknown as { ɵloadPlugin: (m: unknown) => void }
|
||||
).ɵloadPlugin({
|
||||
pluginId: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Plugin API Test Suite (CI)',
|
||||
code,
|
||||
icon: '',
|
||||
description: '',
|
||||
permissions,
|
||||
});
|
||||
},
|
||||
{ code: injectedCode, permissions: PERMISSIONS },
|
||||
);
|
||||
|
||||
await Promise.race([
|
||||
done,
|
||||
new Promise<void>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error('Timed out waiting for test results')),
|
||||
120000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
await browser.close();
|
||||
server?.kill();
|
||||
|
||||
printReport(results, coverage, skipped);
|
||||
|
||||
if (fatal) {
|
||||
console.error(`\nFatal error while running tests: ${fatal}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const failed = results.filter((r) => r.status === 'fail').length;
|
||||
const passed = results.filter((r) => r.status === 'pass').length;
|
||||
console.log(
|
||||
`\n${passed} passed, ${failed} failed${
|
||||
skipped.length ? `, ${skipped.length} skipped` : ''
|
||||
}.`,
|
||||
);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
27
plugins/apps/plugin-api-test-suite/eslint.config.js
Normal file
27
plugins/apps/plugin-api-test-suite/eslint.config.js
Normal file
@ -0,0 +1,27 @@
|
||||
import baseConfig from '../../eslint.config.js';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.*?.json',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'**/assets/*.js',
|
||||
'vite.config.ts',
|
||||
'vite.config.headless.ts',
|
||||
'vite.config.tests.ts',
|
||||
'vite.config.iife.ts',
|
||||
],
|
||||
},
|
||||
];
|
||||
12
plugins/apps/plugin-api-test-suite/index.html
Normal file
12
plugins/apps/plugin-api-test-suite/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Plugin API Test Suite</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<main id="app" class="wrapper"></main>
|
||||
<script type="module" src="/src/ui.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
plugins/apps/plugin-api-test-suite/package.json
Normal file
22
plugins/apps/plugin-api-test-suite/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "plugin-api-test-suite",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build --emptyOutDir && pnpm run build:headless && pnpm run build:tests",
|
||||
"build:headless": "vite build --config vite.config.headless.ts",
|
||||
"build:tests": "vite build --config vite.config.tests.ts",
|
||||
"watch": "concurrently --kill-others --names app,tests \"vite build --watch --mode development\" \"vite build --watch --mode development --config vite.config.tests.ts\"",
|
||||
"serve": "vite preview",
|
||||
"init": "concurrently --kill-others --names build,serve \"pnpm run watch\" \"pnpm run serve\"",
|
||||
"lint": "eslint .",
|
||||
"gen:api": "tsx tools/gen-api-surface.ts",
|
||||
"test:ci": "pnpm run build:headless && tsx ci/run-ci.ts",
|
||||
"test:ci:mocked": "pnpm run build:headless && MOCK_BACKEND=1 tsx ci/run-ci.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"playwright": "^1.61.0"
|
||||
}
|
||||
}
|
||||
4
plugins/apps/plugin-api-test-suite/public/_headers
Normal file
4
plugins/apps/plugin-api-test-suite/public/_headers
Normal file
@ -0,0 +1,4 @@
|
||||
/*
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||
Access-Control-Allow-Headers: Content-Type
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user