mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-11 09:42:22 +00:00
Compare commits
11 Commits
drjkl/clea
...
test/compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a984620a | ||
|
|
60c683e090 | ||
|
|
0007744169 | ||
|
|
ea5238242c | ||
|
|
c8aa2d569c | ||
|
|
54bf7496f8 | ||
|
|
51ccc2d581 | ||
|
|
3926c60ac4 | ||
|
|
fae7f005db | ||
|
|
aaa861694e | ||
|
|
e30d6cfbbc |
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
|
||||
"entry": ["src/main.ts", "index.html"],
|
||||
"duplicates": {
|
||||
"minOccurrences": 3,
|
||||
"ignore": ["**/*.generated.*", "**/generatedManagerTypes.ts"]
|
||||
},
|
||||
"rules": {}
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,7 +16,6 @@ yarn.lock
|
||||
.eslintcache
|
||||
.prettiercache
|
||||
.stylelintcache
|
||||
.fallow/
|
||||
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
@@ -41,7 +41,10 @@ const config: KnipConfig = {
|
||||
ignoreDependencies: [
|
||||
// Weird importmap things
|
||||
'@iconify-json/lucide',
|
||||
'@iconify/json'
|
||||
'@iconify/json',
|
||||
'@primeuix/forms',
|
||||
'@primeuix/styled',
|
||||
'@primevue/icons'
|
||||
],
|
||||
ignore: [
|
||||
// Auto generated API types
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
"dev:test": "cross-env VITE_USE_LEGACY_DEFAULT_GRAPH=true vite --config vite.config.mts",
|
||||
"dev": "vite --config vite.config.mts",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"fallow": "fallow",
|
||||
"fallow:audit": "fallow audit",
|
||||
"format:check": "oxfmt --check",
|
||||
"format": "oxfmt --write",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
@@ -76,9 +74,12 @@
|
||||
"@customerio/cdp-analytics-browser": "catalog:",
|
||||
"@formkit/auto-animate": "catalog:",
|
||||
"@iconify/json": "catalog:",
|
||||
"@primeuix/forms": "catalog:",
|
||||
"@primeuix/styled": "catalog:",
|
||||
"@primeuix/utils": "catalog:",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/forms": "catalog:",
|
||||
"@primevue/icons": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
"@sentry/vue": "catalog:",
|
||||
"@sparkjsdev/spark": "catalog:",
|
||||
@@ -170,12 +171,12 @@
|
||||
"eslint-plugin-testing-library": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"fallow": "catalog:",
|
||||
"fast-check": "catalog:",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globals": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"husky": "catalog:",
|
||||
"jiti": "catalog:",
|
||||
"jsdom": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
|
||||
371
pnpm-lock.yaml
generated
371
pnpm-lock.yaml
generated
@@ -60,6 +60,12 @@ catalogs:
|
||||
'@playwright/test':
|
||||
specifier: ^1.58.1
|
||||
version: 1.58.1
|
||||
'@primeuix/forms':
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2
|
||||
'@primeuix/styled':
|
||||
specifier: 0.3.2
|
||||
version: 0.3.2
|
||||
'@primeuix/utils':
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.2
|
||||
@@ -69,6 +75,9 @@ catalogs:
|
||||
'@primevue/forms':
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
'@primevue/icons':
|
||||
specifier: 4.2.5
|
||||
version: 4.2.5
|
||||
'@primevue/themes':
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
@@ -228,9 +237,6 @@ catalogs:
|
||||
eslint-plugin-vue:
|
||||
specifier: ^10.9.1
|
||||
version: 10.9.1
|
||||
fallow:
|
||||
specifier: ^2.102.0
|
||||
version: 2.102.0
|
||||
fast-check:
|
||||
specifier: ^4.5.3
|
||||
version: 4.5.3
|
||||
@@ -252,6 +258,9 @@ catalogs:
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
jiti:
|
||||
specifier: 2.6.1
|
||||
version: 2.6.1
|
||||
jsdom:
|
||||
specifier: ^27.4.0
|
||||
version: 27.4.0
|
||||
@@ -409,6 +418,7 @@ catalogs:
|
||||
overrides:
|
||||
vite: ^8.0.13
|
||||
'@tiptap/pm': 2.27.2
|
||||
'@types/eslint': '-'
|
||||
lodash: ^4.18.0
|
||||
yaml: ^2.8.3
|
||||
minimatch@^9.0.0: ^9.0.7
|
||||
@@ -458,6 +468,12 @@ importers:
|
||||
'@iconify/json':
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.380
|
||||
'@primeuix/forms':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.2
|
||||
'@primeuix/styled':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.2
|
||||
'@primeuix/utils':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.2
|
||||
@@ -467,6 +483,9 @@ importers:
|
||||
'@primevue/forms':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.5(vue@3.5.34(typescript@5.9.3))
|
||||
'@primevue/icons':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.5(vue@3.5.34(typescript@5.9.3))
|
||||
'@primevue/themes':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.5
|
||||
@@ -629,10 +648,10 @@ importers:
|
||||
version: link:packages/ingest-types
|
||||
'@eslint/js':
|
||||
specifier: 'catalog:'
|
||||
version: 10.0.1(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 10.0.1(eslint@10.4.0(jiti@2.6.1))
|
||||
'@intlify/eslint-plugin-vue-i18n':
|
||||
specifier: 'catalog:'
|
||||
version: 4.5.0(eslint@10.4.0(jiti@2.7.0))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)))(yaml-eslint-parser@1.3.0)
|
||||
version: 4.5.0(eslint@10.4.0(jiti@2.6.1))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.6.1)))(yaml-eslint-parser@1.3.0)
|
||||
'@lobehub/i18n-cli':
|
||||
specifier: 'catalog:'
|
||||
version: 1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.21.0)(zod@3.25.76)
|
||||
@@ -647,7 +666,7 @@ importers:
|
||||
version: 4.6.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
version: 10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/addon-mcp':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.6(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
@@ -656,10 +675,10 @@ importers:
|
||||
version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vue@3.5.34(typescript@5.9.3))
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
version: 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.3.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
version: 4.3.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: 'catalog:'
|
||||
version: 6.9.1
|
||||
@@ -689,7 +708,7 @@ importers:
|
||||
version: 0.184.1
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
version: 6.0.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.16(vitest@4.1.8)
|
||||
@@ -704,40 +723,37 @@ importers:
|
||||
version: 10.1.0
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 10.4.0(jiti@2.7.0)
|
||||
version: 10.4.0(jiti@2.6.1)
|
||||
eslint-config-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.8(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 10.1.8(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1)))(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-plugin-better-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.3.1(eslint@10.4.0(jiti@2.7.0))(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(tailwindcss@4.3.0)(typescript@5.9.3)
|
||||
version: 4.3.1(eslint@10.4.0(jiti@2.6.1))(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(tailwindcss@4.3.0)(typescript@5.9.3)
|
||||
eslint-plugin-import-x:
|
||||
specifier: 'catalog:'
|
||||
version: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.69.0(oxlint@1.69.0(oxlint-tsgolint@0.23.0))
|
||||
eslint-plugin-playwright:
|
||||
specifier: 'catalog:'
|
||||
version: 2.10.1(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 2.10.1(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.10(eslint@10.4.0(jiti@2.7.0))(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
version: 10.2.10(eslint@10.4.0(jiti@2.6.1))(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
eslint-plugin-testing-library:
|
||||
specifier: 'catalog:'
|
||||
version: 7.16.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
version: 7.16.1(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: 'catalog:'
|
||||
version: 4.4.1(@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 4.4.1(@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-plugin-vue:
|
||||
specifier: 'catalog:'
|
||||
version: 10.9.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)))
|
||||
fallow:
|
||||
specifier: 'catalog:'
|
||||
version: 2.102.0
|
||||
version: 10.9.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.6.1)))
|
||||
fast-check:
|
||||
specifier: 'catalog:'
|
||||
version: 4.5.3
|
||||
@@ -753,6 +769,9 @@ importers:
|
||||
husky:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.7
|
||||
jiti:
|
||||
specifier: 'catalog:'
|
||||
version: 2.6.1
|
||||
jsdom:
|
||||
specifier: 'catalog:'
|
||||
version: 27.4.0
|
||||
@@ -809,7 +828,7 @@ importers:
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
version: 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
unplugin-icons:
|
||||
specifier: 'catalog:'
|
||||
version: 22.5.0(@vue/compiler-sfc@3.5.34)
|
||||
@@ -824,25 +843,25 @@ importers:
|
||||
version: 11.1.1
|
||||
vite:
|
||||
specifier: ^8.0.13
|
||||
version: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
version: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-dts:
|
||||
specifier: 'catalog:'
|
||||
version: 4.5.4(@types/node@24.10.4)(typescript@5.9.3)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
version: 4.5.4(@types/node@24.10.4)(typescript@5.9.3)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-html:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
version: 3.2.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
version: 8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vue-component-type-helpers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.2
|
||||
vue-eslint-parser:
|
||||
specifier: 'catalog:'
|
||||
version: 10.4.0(eslint@10.4.0(jiti@2.7.0))
|
||||
version: 10.4.0(eslint@10.4.0(jiti@2.6.1))
|
||||
vue-tsc:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.5(typescript@5.9.3)
|
||||
@@ -1864,46 +1883,6 @@ packages:
|
||||
'@exodus/crypto':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/darwin-arm64@2.102.0':
|
||||
resolution: {integrity: sha512-B8wzfzJgoX6h5Gv2xQ9ZidO5Jb8/PWdssAxYbWs1pb5oJHZ6S5PLwXuUdINmNSIaRGQwTk4DC9/tIMFHFvd9uw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@fallow-cli/darwin-x64@2.102.0':
|
||||
resolution: {integrity: sha512-aLbTWWzQnleKdi56obAPXMJm7YA3qAnkIX9T3eocRHiagYqp8nsf4cslM0rZKvu2WwK34NaBm8x886gl4cl+zg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@fallow-cli/linux-arm64-gnu@2.102.0':
|
||||
resolution: {integrity: sha512-8nYeOSLSewqcKH/KUcKZaCq5QII5VTRX62l60B6UM1iKe/0jcmlQ2mOtx4rkHpeq3LL5emvT/ph4NNgGmWKSBg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@fallow-cli/linux-arm64-musl@2.102.0':
|
||||
resolution: {integrity: sha512-2Zi33PXzZxD7HU5sPVwyElZGP7zyEdGo4hK0ewy6gMYgQ9BDfLnFhgaSSOzN1J4paIhYtBmVsLmqakyDKy22Jw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@fallow-cli/linux-x64-gnu@2.102.0':
|
||||
resolution: {integrity: sha512-7Hys4X6hKuR/lqUaGXwezRzDrwXwu9KfahUy85WTuiG1to+ZbzDCqdbZ04LtnI8kK8ufrPDcq+ZXdyt5ksOJHA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@fallow-cli/linux-x64-musl@2.102.0':
|
||||
resolution: {integrity: sha512-RuDY1jOEPgJOuHBgEpHVl6J7Xf2QLFklnNy6zO1nw8R1fLgWUAAKlFirn1Y93pb9bXzqpn5Gre3YlhgIZ3+LBA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@fallow-cli/win32-arm64-msvc@2.102.0':
|
||||
resolution: {integrity: sha512-wsvHjLzWFvsYmCqnQLm1doSEHb9Z038+sFp1RzMcffPUBC5tqS3vCr8J8nolcLlPk7o3QWHQMXAqb1xSJe+doA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@fallow-cli/win32-x64-msvc@2.102.0':
|
||||
resolution: {integrity: sha512-rH1hd0PD0mm6pCxh1pw5jubpJsvV6f5rjixMoD5AZLzWa6NPJBpPPuzruLGtH/9CYy8B0y7zPrGGTKRO2PCGzg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@firebase/analytics-compat@0.2.18':
|
||||
resolution: {integrity: sha512-Hw9mzsSMZaQu6wrTbi3kYYwGw9nBqOHr47pVLxfr5v8CalsdrG5gfs9XUlPOZjHRVISp3oQrh1j7d3E+ulHPjQ==}
|
||||
peerDependencies:
|
||||
@@ -5635,11 +5614,6 @@ packages:
|
||||
extendable-media-recorder@9.2.27:
|
||||
resolution: {integrity: sha512-2X+Ixi1cxLek0Cj9x9atmhQ+apG+LwJpP2p3ypP8Pxau0poDnicrg7FTfPVQV5PW/3DHFm/eQ16vbgo5Yk3HGQ==}
|
||||
|
||||
fallow@2.102.0:
|
||||
resolution: {integrity: sha512-bkOT58kPVCB12d2apQjIKBw/qSdsGRPQFrN5ff9Yl5WzXRqlDTbT/MVdMXld4sJD5JQW1ftw2bTxJWCINggh6g==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
fast-check@4.5.3:
|
||||
resolution: {integrity: sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==}
|
||||
engines: {node: '>=12.17.0'}
|
||||
@@ -6334,6 +6308,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
jiti@2.6.1:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jiti@2.7.0:
|
||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||
hasBin: true
|
||||
@@ -9830,9 +9808,9 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@2.7.0))':
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@2.6.1))':
|
||||
dependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
@@ -9872,9 +9850,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/js@10.0.1(eslint@10.4.0(jiti@2.7.0))':
|
||||
'@eslint/js@10.0.1(eslint@10.4.0(jiti@2.6.1))':
|
||||
optionalDependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
|
||||
'@eslint/object-schema@3.0.5': {}
|
||||
|
||||
@@ -9885,30 +9863,6 @@ snapshots:
|
||||
|
||||
'@exodus/bytes@1.7.0': {}
|
||||
|
||||
'@fallow-cli/darwin-arm64@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/darwin-x64@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/linux-arm64-gnu@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/linux-arm64-musl@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/linux-x64-gnu@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/linux-x64-musl@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/win32-arm64-msvc@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@fallow-cli/win32-x64-msvc@2.102.0':
|
||||
optional: true
|
||||
|
||||
'@firebase/analytics-compat@0.2.18(@firebase/app-compat@0.2.53)(@firebase/app@0.11.4)':
|
||||
dependencies:
|
||||
'@firebase/analytics': 0.10.12(@firebase/app@0.11.4)
|
||||
@@ -10467,14 +10421,14 @@ snapshots:
|
||||
'@intlify/message-compiler': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
|
||||
'@intlify/eslint-plugin-vue-i18n@4.5.0(eslint@10.4.0(jiti@2.7.0))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)))(yaml-eslint-parser@1.3.0)':
|
||||
'@intlify/eslint-plugin-vue-i18n@4.5.0(eslint@10.4.0(jiti@2.6.1))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.6.1)))(yaml-eslint-parser@1.3.0)':
|
||||
dependencies:
|
||||
'@eslint/eslintrc': 3.3.3
|
||||
'@intlify/core-base': 11.2.8
|
||||
'@intlify/message-compiler': 11.2.8
|
||||
debug: 4.4.3
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint-compat-utils: 0.6.5(eslint@10.4.0(jiti@2.7.0))
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
eslint-compat-utils: 0.6.5(eslint@10.4.0(jiti@2.6.1))
|
||||
glob: 10.5.0
|
||||
globals: 16.5.0
|
||||
ignore: 7.0.5
|
||||
@@ -10486,7 +10440,7 @@ snapshots:
|
||||
parse5: 7.3.0
|
||||
semver: 7.7.4
|
||||
synckit: 0.11.12
|
||||
vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0))
|
||||
vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.6.1))
|
||||
yaml-eslint-parser: 1.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11419,10 +11373,10 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@storybook/addon-docs@10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@storybook/addon-docs@10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.1.9)(react@19.2.4)
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@storybook/react-dom-shim': 10.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
react: 19.2.4
|
||||
@@ -11448,24 +11402,24 @@ snapshots:
|
||||
- '@tmcp/auth'
|
||||
- typescript
|
||||
|
||||
'@storybook/builder-vite@10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@storybook/builder-vite@10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- esbuild
|
||||
- rollup
|
||||
- webpack
|
||||
|
||||
'@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.3
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
@@ -11490,14 +11444,14 @@ snapshots:
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
||||
'@storybook/vue3-vite@10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
'@storybook/vue3-vite@10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/vue3': 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vue@3.5.34(typescript@5.9.3))
|
||||
magic-string: 0.30.21
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
typescript: 5.9.3
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue-component-meta: 2.2.12(typescript@5.9.3)
|
||||
vue-docgen-api: 4.79.2(vue@3.5.34(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
@@ -11579,12 +11533,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
|
||||
|
||||
'@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.3.0
|
||||
'@tailwindcss/oxide': 4.3.0
|
||||
tailwindcss: 4.3.0
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
@@ -11950,15 +11904,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.60.0
|
||||
'@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.60.0
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
@@ -11966,14 +11920,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.60.0
|
||||
'@typescript-eslint/types': 8.60.0
|
||||
'@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.60.0
|
||||
debug: 4.4.3
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11996,13 +11950,13 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.60.0
|
||||
'@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -12025,13 +11979,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.6.1))
|
||||
'@typescript-eslint/scope-manager': 8.60.0
|
||||
'@typescript-eslint/types': 8.60.0
|
||||
'@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -12132,10 +12086,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
@@ -12163,7 +12117,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 3.10.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12184,13 +12138,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.8(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@vitest/mocker@4.1.8(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.8
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@vitest/mocker@4.1.8(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
@@ -12239,7 +12193,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
@@ -12419,14 +12373,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
|
||||
'@vue/devtools-core@8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
'@vue/devtools-core@8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
mitt: 3.0.1
|
||||
nanoid: 5.1.5
|
||||
pathe: 2.0.3
|
||||
vite-hot-client: 2.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-hot-client: 2.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@@ -13626,14 +13580,14 @@ snapshots:
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
eslint-compat-utils@0.6.5(eslint@10.4.0(jiti@2.7.0)):
|
||||
eslint-compat-utils@0.6.5(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
semver: 7.7.4
|
||||
|
||||
eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)):
|
||||
eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
|
||||
eslint-import-context@0.1.9(unrs-resolver@1.11.1):
|
||||
dependencies:
|
||||
@@ -13642,10 +13596,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
unrs-resolver: 1.11.1
|
||||
|
||||
eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0)):
|
||||
eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1)))(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
||||
get-tsconfig: 4.14.0
|
||||
is-bun-module: 2.0.0
|
||||
@@ -13653,11 +13607,11 @@ snapshots:
|
||||
tinyglobby: 0.2.16
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))
|
||||
eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-better-tailwindcss@4.3.1(eslint@10.4.0(jiti@2.7.0))(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(tailwindcss@4.3.0)(typescript@5.9.3):
|
||||
eslint-plugin-better-tailwindcss@4.3.1(eslint@10.4.0(jiti@2.6.1))(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(tailwindcss@4.3.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@eslint/css-tree': 3.6.9
|
||||
'@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3))
|
||||
@@ -13669,18 +13623,18 @@ snapshots:
|
||||
tsconfig-paths-webpack-plugin: 4.2.0
|
||||
valibot: 1.2.0(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
oxlint: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0)):
|
||||
eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@package-json/types': 0.0.12
|
||||
'@typescript-eslint/types': 8.60.0
|
||||
comment-parser: 1.4.1
|
||||
debug: 4.4.3
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.9
|
||||
@@ -13688,7 +13642,7 @@ snapshots:
|
||||
stable-hash-x: 0.2.0
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -13697,47 +13651,47 @@ snapshots:
|
||||
jsonc-parser: 3.3.1
|
||||
oxlint: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
|
||||
eslint-plugin-playwright@2.10.1(eslint@10.4.0(jiti@2.7.0)):
|
||||
eslint-plugin-playwright@2.10.1(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
globals: 17.4.0
|
||||
|
||||
eslint-plugin-storybook@10.2.10(eslint@10.4.0(jiti@2.7.0))(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
|
||||
eslint-plugin-storybook@10.2.10(eslint@10.4.0(jiti@2.6.1))(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-testing-library@7.16.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3):
|
||||
eslint-plugin-testing-library@7.16.1(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.60.0
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0)):
|
||||
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
||||
eslint-plugin-vue@10.9.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))):
|
||||
eslint-plugin-vue@10.9.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.6.1))):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
natural-compare: 1.4.0
|
||||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 7.1.1
|
||||
semver: 7.7.4
|
||||
vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0))
|
||||
vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.6.1))
|
||||
xml-name-validator: 4.0.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
||||
eslint-scope@9.1.2:
|
||||
dependencies:
|
||||
@@ -13752,9 +13706,9 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@10.4.0(jiti@2.7.0):
|
||||
eslint@10.4.0(jiti@2.6.1):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.6.1))
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.23.5
|
||||
'@eslint/config-helpers': 0.6.0
|
||||
@@ -13785,7 +13739,7 @@ snapshots:
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.4
|
||||
optionalDependencies:
|
||||
jiti: 2.7.0
|
||||
jiti: 2.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -13875,19 +13829,6 @@ snapshots:
|
||||
subscribable-things: 2.1.53
|
||||
tslib: 2.8.1
|
||||
|
||||
fallow@2.102.0:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
'@fallow-cli/darwin-arm64': 2.102.0
|
||||
'@fallow-cli/darwin-x64': 2.102.0
|
||||
'@fallow-cli/linux-arm64-gnu': 2.102.0
|
||||
'@fallow-cli/linux-arm64-musl': 2.102.0
|
||||
'@fallow-cli/linux-x64-gnu': 2.102.0
|
||||
'@fallow-cli/linux-x64-musl': 2.102.0
|
||||
'@fallow-cli/win32-arm64-msvc': 2.102.0
|
||||
'@fallow-cli/win32-x64-msvc': 2.102.0
|
||||
|
||||
fast-check@4.5.3:
|
||||
dependencies:
|
||||
pure-rand: 7.0.1
|
||||
@@ -14672,6 +14613,8 @@ snapshots:
|
||||
filelist: 1.0.4
|
||||
minimatch: 3.1.5
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jiti@2.7.0: {}
|
||||
|
||||
jju@1.4.0: {}
|
||||
@@ -17038,13 +16981,13 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3):
|
||||
typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
'@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -17294,11 +17237,11 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-dev-rpc@1.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
@@ -17306,15 +17249,15 @@ snapshots:
|
||||
vite: 8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-hot-client@2.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
vite: 8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-plugin-dts@4.5.4(@types/node@24.10.4)(typescript@5.9.3)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-plugin-dts@4.5.4(@types/node@24.10.4)(typescript@5.9.3)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.57.2(@types/node@24.10.4)
|
||||
'@rollup/pluginutils': 5.3.0
|
||||
@@ -17327,13 +17270,13 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
typescript: 5.9.3
|
||||
optionalDependencies:
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-html@3.2.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-plugin-html@3.2.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -17347,7 +17290,7 @@ snapshots:
|
||||
html-minifier-terser: 6.1.0
|
||||
node-html-parser: 5.4.2
|
||||
pathe: 0.2.0
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-plugin-html@3.2.2(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
@@ -17365,7 +17308,7 @@ snapshots:
|
||||
pathe: 0.2.0
|
||||
vite: 8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -17375,8 +17318,8 @@ snapshots:
|
||||
perfect-debounce: 2.0.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -17395,15 +17338,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)):
|
||||
vite-plugin-vue-devtools@8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
'@vue/devtools-core': 8.0.5(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
@@ -17437,7 +17380,7 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -17448,7 +17391,7 @@ snapshots:
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -17482,7 +17425,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
@@ -17493,7 +17436,7 @@ snapshots:
|
||||
'@types/node': 24.10.4
|
||||
esbuild: 0.27.3
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.7.0
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.9.0
|
||||
@@ -17518,10 +17461,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/coverage-v8@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.8
|
||||
'@vitest/mocker': 4.1.8(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@vitest/mocker': 4.1.8(vite@8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.1.8
|
||||
'@vitest/runner': 4.1.8
|
||||
'@vitest/snapshot': 4.1.8
|
||||
@@ -17538,7 +17481,7 @@ snapshots:
|
||||
tinyexec: 1.0.4
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite: 8.0.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -17716,10 +17659,10 @@ snapshots:
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.34(typescript@5.9.3))
|
||||
|
||||
vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)):
|
||||
vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
eslint: 10.4.0(jiti@2.7.0)
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
eslint-scope: 9.1.2
|
||||
eslint-visitor-keys: 5.0.1
|
||||
espree: 11.2.0
|
||||
|
||||
@@ -28,9 +28,12 @@ catalog:
|
||||
'@lucide/vue': ^1.17.0
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@playwright/test': ^1.58.1
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
'@primevue/core': ^4.2.5
|
||||
'@primevue/forms': ^4.2.5
|
||||
'@primevue/icons': 4.2.5
|
||||
'@primevue/themes': ^4.2.5
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
'@sentry/vue': ^10.32.1
|
||||
@@ -85,7 +88,6 @@ catalog:
|
||||
eslint-plugin-testing-library: ^7.16.1
|
||||
eslint-plugin-unused-imports: ^4.4.1
|
||||
eslint-plugin-vue: ^10.9.1
|
||||
fallow: ^2.102.0
|
||||
fast-check: ^4.5.3
|
||||
firebase: ^11.6.0
|
||||
glob: ^13.0.6
|
||||
@@ -93,6 +95,7 @@ catalog:
|
||||
gsap: ^3.14.2
|
||||
happy-dom: ^20.8.9
|
||||
husky: ^9.1.7
|
||||
jiti: 2.6.1
|
||||
jsdom: ^27.4.0
|
||||
jsonata: ^2.1.0
|
||||
knip: ^6.15.0
|
||||
@@ -163,6 +166,7 @@ allowBuilds:
|
||||
overrides:
|
||||
vite: 'catalog:'
|
||||
'@tiptap/pm': 2.27.2
|
||||
'@types/eslint': '-'
|
||||
# Security overrides
|
||||
lodash: ^4.18.0
|
||||
yaml: ^2.8.3
|
||||
|
||||
@@ -113,3 +113,13 @@ export const formatUsdFromCents = ({
|
||||
locale,
|
||||
numberOptions
|
||||
})
|
||||
|
||||
/**
|
||||
* Clamps a USD value to the allowed range for credit purchases
|
||||
* @param value - The USD amount to clamp
|
||||
* @returns The clamped value between $1 and $1000, or 0 if NaN
|
||||
*/
|
||||
export const clampUsd = (value: number): number => {
|
||||
if (Number.isNaN(value)) return 0
|
||||
return Math.min(1000, Math.max(1, value))
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ vi.mock('@/scripts/app', () => ({
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
getNodeByExecutionId: vi.fn(),
|
||||
getRootParentNode: vi.fn(() => null),
|
||||
forEachNode: vi.fn(),
|
||||
mapAllNodes: vi.fn(() => [])
|
||||
}))
|
||||
|
||||
@@ -17,6 +17,7 @@ vi.mock('@/scripts/app', () => ({
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
getNodeByExecutionId: vi.fn(),
|
||||
getExecutionIdByNode: vi.fn(),
|
||||
getRootParentNode: vi.fn(() => null),
|
||||
forEachNode: vi.fn(),
|
||||
mapAllNodes: vi.fn(() => [])
|
||||
}))
|
||||
|
||||
@@ -17,6 +17,7 @@ vi.mock('@/scripts/app', () => ({
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
getNodeByExecutionId: vi.fn(),
|
||||
getExecutionIdByNode: vi.fn(),
|
||||
getRootParentNode: vi.fn(() => null),
|
||||
forEachNode: vi.fn(),
|
||||
mapAllNodes: vi.fn(() => [])
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { defineComponent, h, markRaw, ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h, markRaw, nextTick, ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
@@ -12,19 +13,35 @@ import {
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { toNodeId } from '@/types/nodeId'
|
||||
import { createMockPositionable } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
const mockApp = vi.hoisted(() => ({
|
||||
canvas: null
|
||||
}))
|
||||
|
||||
const mockFeatureFlags = vi.hoisted(() => ({
|
||||
refs: null as null | {
|
||||
shouldRenderVueNodes: { value: boolean }
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({ app: mockApp }))
|
||||
|
||||
vi.mock('@/composables/useVueFeatureFlags', () => ({
|
||||
useVueFeatureFlags: () => ({
|
||||
shouldRenderVueNodes: { value: false }
|
||||
})
|
||||
}))
|
||||
vi.mock('@/composables/useVueFeatureFlags', async () => {
|
||||
const { ref } = await import('vue')
|
||||
const shouldRenderVueNodes = ref(false)
|
||||
mockFeatureFlags.refs = {
|
||||
shouldRenderVueNodes
|
||||
}
|
||||
|
||||
return {
|
||||
useVueFeatureFlags: () => ({
|
||||
shouldRenderVueNodes
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('useSelectionToolboxPosition', () => {
|
||||
let canvasStore: ReturnType<typeof useCanvasStore>
|
||||
@@ -32,28 +49,31 @@ describe('useSelectionToolboxPosition', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
canvasStore = useCanvasStore()
|
||||
layoutStore.initializeFromLiteGraph([])
|
||||
layoutStore.isDraggingVueNodes.value = false
|
||||
if (mockFeatureFlags.refs) {
|
||||
mockFeatureFlags.refs.shouldRenderVueNodes.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function renderToolboxForSelection(item: Positionable) {
|
||||
canvasStore.canvas = markRaw({
|
||||
canvas: document.createElement('canvas'),
|
||||
ds: {
|
||||
offset: [0, 0],
|
||||
scale: 1
|
||||
},
|
||||
selectedItems: new Set([item]),
|
||||
state: {
|
||||
draggingItems: false,
|
||||
selectionChanged: true
|
||||
}
|
||||
} as Partial<LGraphCanvas> as LGraphCanvas)
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function renderToolboxForSelection(
|
||||
items: Iterable<Positionable>,
|
||||
state: Partial<LGraphCanvas['state']> = {},
|
||||
ds: Partial<LGraphCanvas['ds']> = {}
|
||||
) {
|
||||
setCanvasSelection(items, state, ds)
|
||||
|
||||
let toolbox: HTMLElement | undefined
|
||||
let visible!: ReturnType<typeof useSelectionToolboxPosition>['visible']
|
||||
const TestHarness = defineComponent({
|
||||
setup() {
|
||||
const toolboxRef = ref<HTMLElement>(document.createElement('div'))
|
||||
toolbox = toolboxRef.value
|
||||
useSelectionToolboxPosition(toolboxRef)
|
||||
;({ visible } = useSelectionToolboxPosition(toolboxRef))
|
||||
return () => h('div')
|
||||
}
|
||||
})
|
||||
@@ -61,7 +81,31 @@ describe('useSelectionToolboxPosition', () => {
|
||||
const wrapper = render(TestHarness)
|
||||
if (!toolbox) throw new Error('Toolbox element was not initialized')
|
||||
|
||||
return { toolbox, unmount: wrapper.unmount }
|
||||
if (!visible) throw new Error('Visible state was not initialized')
|
||||
|
||||
return { toolbox, unmount: wrapper.unmount, visible }
|
||||
}
|
||||
|
||||
function setCanvasSelection(
|
||||
items: Iterable<Positionable>,
|
||||
state: Partial<LGraphCanvas['state']> = {},
|
||||
ds: Partial<LGraphCanvas['ds']> = {}
|
||||
) {
|
||||
canvasStore.canvas = markRaw(
|
||||
fromPartial<LGraphCanvas>({
|
||||
canvas: document.createElement('canvas'),
|
||||
ds: {
|
||||
offset: ds.offset ?? [0, 0],
|
||||
scale: ds.scale ?? 1
|
||||
},
|
||||
selectedItems: new Set(items),
|
||||
state: {
|
||||
draggingItems: false,
|
||||
selectionChanged: true,
|
||||
...state
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
it('positions groups from their unchanged bounds', () => {
|
||||
@@ -69,7 +113,7 @@ describe('useSelectionToolboxPosition', () => {
|
||||
group.pos = [100, 200]
|
||||
group.size = [160, 80]
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection(group)
|
||||
const { toolbox, unmount } = renderToolboxForSelection([group])
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('190px')
|
||||
unmount()
|
||||
@@ -81,11 +125,222 @@ describe('useSelectionToolboxPosition', () => {
|
||||
node.pos = [100, 200]
|
||||
node.size = [160, 80]
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection(node)
|
||||
const { toolbox, unmount } = renderToolboxForSelection([node])
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe(
|
||||
`${190 - LiteGraph.NODE_TITLE_HEIGHT}px`
|
||||
)
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('does not set coordinates when selection is empty', () => {
|
||||
const { toolbox, visible, unmount } = renderToolboxForSelection([])
|
||||
|
||||
expect(visible.value).toBe(false)
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('does not update when selection state is unchanged', () => {
|
||||
const group = new LGraphGroup('Group', 1)
|
||||
group.pos = [100, 200]
|
||||
group.size = [160, 80]
|
||||
|
||||
const { toolbox, visible, unmount } = renderToolboxForSelection([group], {
|
||||
selectionChanged: false
|
||||
})
|
||||
|
||||
expect(visible.value).toBe(false)
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('does not set coordinates while selected items are being dragged', () => {
|
||||
const group = new LGraphGroup('Group', 1)
|
||||
group.pos = [100, 200]
|
||||
group.size = [160, 80]
|
||||
|
||||
const { toolbox, visible, unmount } = renderToolboxForSelection([group], {
|
||||
draggingItems: true
|
||||
})
|
||||
|
||||
expect(visible.value).toBe(false)
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('positions multiple selected items from their union bounds', () => {
|
||||
const first = new LGraphGroup('First', 1)
|
||||
first.pos = [100, 200]
|
||||
first.size = [100, 40]
|
||||
const second = new LGraphGroup('Second', 2)
|
||||
second.pos = [300, 260]
|
||||
second.size = [50, 40]
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection([first, second])
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('270px')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('190px')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('applies canvas scale and offset to screen coordinates', () => {
|
||||
const group = new LGraphGroup('Group', 1)
|
||||
group.pos = [100, 200]
|
||||
group.size = [100, 40]
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection(
|
||||
[group],
|
||||
{},
|
||||
{ offset: [10, 20], scale: 2 }
|
||||
)
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('360px')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('420px')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('uses Vue layout bounds when Vue node rendering is enabled', () => {
|
||||
if (!mockFeatureFlags.refs) {
|
||||
throw new Error('feature flag refs were not initialized')
|
||||
}
|
||||
mockFeatureFlags.refs.shouldRenderVueNodes.value = true
|
||||
const node = new LGraphNode('Node')
|
||||
node.id = toNodeId(12)
|
||||
node.pos = [100, 200]
|
||||
node.size = [160, 80]
|
||||
layoutStore.initializeFromLiteGraph([
|
||||
{
|
||||
id: node.id,
|
||||
pos: [300, 400],
|
||||
size: [200, 120]
|
||||
}
|
||||
])
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection([node])
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('400px')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe(
|
||||
`${390 - LiteGraph.NODE_TITLE_HEIGHT}px`
|
||||
)
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('falls back to LiteGraph node bounds when Vue layout is missing', () => {
|
||||
if (!mockFeatureFlags.refs) {
|
||||
throw new Error('feature flag refs were not initialized')
|
||||
}
|
||||
mockFeatureFlags.refs.shouldRenderVueNodes.value = true
|
||||
const node = new LGraphNode('Node')
|
||||
node.id = toNodeId(13)
|
||||
node.pos = [100, 200]
|
||||
node.size = [160, 80]
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection([node])
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe(
|
||||
`${190 - LiteGraph.NODE_TITLE_HEIGHT}px`
|
||||
)
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('hides the toolbox while Vue nodes are being dragged', () => {
|
||||
if (!mockFeatureFlags.refs) {
|
||||
throw new Error('feature flag refs were not initialized')
|
||||
}
|
||||
mockFeatureFlags.refs.shouldRenderVueNodes.value = true
|
||||
layoutStore.isDraggingVueNodes.value = true
|
||||
const group = new LGraphGroup('Group', 1)
|
||||
group.pos = [100, 200]
|
||||
group.size = [160, 80]
|
||||
|
||||
const { toolbox, unmount } = renderToolboxForSelection([group])
|
||||
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('ignores selected items that are not nodes or groups', () => {
|
||||
const item = createMockPositionable({
|
||||
id: toNodeId(52),
|
||||
pos: [100, 200],
|
||||
size: [160, 80],
|
||||
boundingRect: [100, 200, 160, 80]
|
||||
})
|
||||
|
||||
const { toolbox, visible, unmount } = renderToolboxForSelection([item])
|
||||
|
||||
expect(visible.value).toBe(true)
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('ignores selected items without valid ids', () => {
|
||||
const item = fromPartial<Positionable>({
|
||||
pos: [100, 200],
|
||||
size: [160, 80],
|
||||
boundingRect: [100, 200, 160, 80]
|
||||
})
|
||||
|
||||
const { toolbox, visible, unmount } = renderToolboxForSelection([item])
|
||||
|
||||
expect(visible.value).toBe(true)
|
||||
expect(toolbox.style.getPropertyValue('--tb-x')).toBe('')
|
||||
expect(toolbox.style.getPropertyValue('--tb-y')).toBe('')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('stays visible without mutating style when the toolbox ref is empty', () => {
|
||||
const group = new LGraphGroup('Group', 1)
|
||||
group.pos = [100, 200]
|
||||
group.size = [160, 80]
|
||||
setCanvasSelection([group])
|
||||
|
||||
let visible!: ReturnType<typeof useSelectionToolboxPosition>['visible']
|
||||
const TestHarness = defineComponent({
|
||||
setup() {
|
||||
;({ visible } = useSelectionToolboxPosition(ref()))
|
||||
return () => h('div')
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = render(TestHarness)
|
||||
|
||||
expect(visible.value).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('hides and restores around Vue node drag state changes', async () => {
|
||||
if (!mockFeatureFlags.refs) {
|
||||
throw new Error('feature flag refs were not initialized')
|
||||
}
|
||||
mockFeatureFlags.refs.shouldRenderVueNodes.value = true
|
||||
const group = new LGraphGroup('Group', 1)
|
||||
group.pos = [100, 200]
|
||||
group.size = [160, 80]
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
window.setTimeout(() => callback(0), 0)
|
||||
)
|
||||
vi.stubGlobal('cancelAnimationFrame', (handle: number) => {
|
||||
clearTimeout(handle)
|
||||
})
|
||||
|
||||
const { visible, unmount } = renderToolboxForSelection([group])
|
||||
expect(visible.value).toBe(true)
|
||||
|
||||
layoutStore.isDraggingVueNodes.value = true
|
||||
await nextTick()
|
||||
expect(visible.value).toBe(false)
|
||||
|
||||
layoutStore.isDraggingVueNodes.value = false
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
expect(visible.value).toBe(true)
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,30 +10,43 @@ import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { toNodeId } from '@/types/nodeId'
|
||||
|
||||
// canvasStore transitively imports the app singleton; stub it so the real
|
||||
// ComfyApp module never loads during these unit tests.
|
||||
const { actions, customization } = vi.hoisted(() => ({
|
||||
actions: {
|
||||
adjustNodeSize: vi.fn(),
|
||||
toggleNodeCollapse: vi.fn(),
|
||||
toggleNodePin: vi.fn(),
|
||||
toggleNodeBypass: vi.fn(),
|
||||
runBranch: vi.fn()
|
||||
},
|
||||
customization: {
|
||||
shapeOptions: [] as Array<{ localizedName: string; value: string }>,
|
||||
colorOptions: [] as Array<{
|
||||
name: string
|
||||
localizedName: string
|
||||
value: { dark: string; light: string }
|
||||
}>,
|
||||
applyShape: vi.fn(),
|
||||
applyColor: vi.fn(),
|
||||
isLightTheme: { value: false }
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: { canvas: { selected_nodes: null } }
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useNodeCustomization', () => ({
|
||||
useNodeCustomization: () => ({
|
||||
shapeOptions: [],
|
||||
applyShape: vi.fn(),
|
||||
applyColor: vi.fn(),
|
||||
colorOptions: [],
|
||||
isLightTheme: { value: false }
|
||||
shapeOptions: customization.shapeOptions,
|
||||
applyShape: customization.applyShape,
|
||||
applyColor: customization.applyColor,
|
||||
colorOptions: customization.colorOptions,
|
||||
isLightTheme: customization.isLightTheme
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useSelectedNodeActions', () => ({
|
||||
useSelectedNodeActions: () => ({
|
||||
adjustNodeSize: vi.fn(),
|
||||
toggleNodeCollapse: vi.fn(),
|
||||
toggleNodePin: vi.fn(),
|
||||
toggleNodeBypass: vi.fn(),
|
||||
runBranch: vi.fn()
|
||||
})
|
||||
useSelectedNodeActions: () => actions
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
@@ -69,9 +82,29 @@ const getBypassLabel = (selected: LGraphNode[]): string => {
|
||||
return label
|
||||
}
|
||||
|
||||
describe('useNodeMenuOptions.getBypassOption', () => {
|
||||
function readNodeMenuOptions<T>(
|
||||
read: (options: ReturnType<typeof useNodeMenuOptions>) => T
|
||||
): T {
|
||||
const unread = Symbol('unread')
|
||||
const result: { value: T | typeof unread } = { value: unread }
|
||||
const Wrapper = defineComponent({
|
||||
setup() {
|
||||
result.value = read(useNodeMenuOptions())
|
||||
return () => null
|
||||
}
|
||||
})
|
||||
render(Wrapper, { global: { plugins: [i18n] } })
|
||||
if (result.value === unread) throw new Error('Composable was not read')
|
||||
return result.value
|
||||
}
|
||||
|
||||
describe('useNodeMenuOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
customization.shapeOptions = []
|
||||
customization.colorOptions = []
|
||||
customization.isLightTheme.value = false
|
||||
})
|
||||
|
||||
it('labels as "Bypass" when no node is bypassed', () => {
|
||||
@@ -97,4 +130,109 @@ describe('useNodeMenuOptions.getBypassOption', () => {
|
||||
])
|
||||
).toBe('contextMenu.Bypass')
|
||||
})
|
||||
|
||||
it('labels visual node options from the collapsed state and bumps after action', () => {
|
||||
const expandBump = vi.fn()
|
||||
const expand = readNodeMenuOptions(
|
||||
({ getNodeVisualOptions }) =>
|
||||
getNodeVisualOptions({ collapsed: true, pinned: false }, expandBump)[0]
|
||||
)
|
||||
expect(expand).toMatchObject({
|
||||
label: 'contextMenu.Expand Node',
|
||||
icon: 'icon-[lucide--maximize-2]'
|
||||
})
|
||||
expand.action?.()
|
||||
expect(actions.toggleNodeCollapse).toHaveBeenCalledTimes(1)
|
||||
expect(expandBump).toHaveBeenCalledTimes(1)
|
||||
|
||||
const minimize = readNodeMenuOptions(
|
||||
({ getNodeVisualOptions }) =>
|
||||
getNodeVisualOptions({ collapsed: false, pinned: false }, vi.fn())[0]
|
||||
)
|
||||
expect(minimize).toMatchObject({
|
||||
label: 'contextMenu.Minimize Node',
|
||||
icon: 'icon-[lucide--minimize-2]'
|
||||
})
|
||||
})
|
||||
|
||||
it('labels pin options from the pinned state and bumps after action', () => {
|
||||
const bump = vi.fn()
|
||||
const unpin = readNodeMenuOptions(({ getPinOption }) =>
|
||||
getPinOption({ collapsed: false, pinned: true }, bump)
|
||||
)
|
||||
expect(unpin).toMatchObject({
|
||||
label: 'contextMenu.Unpin',
|
||||
icon: 'icon-[lucide--pin-off]'
|
||||
})
|
||||
unpin.action?.()
|
||||
expect(actions.toggleNodePin).toHaveBeenCalledTimes(1)
|
||||
expect(bump).toHaveBeenCalledTimes(1)
|
||||
|
||||
const pin = readNodeMenuOptions(({ getPinOption }) =>
|
||||
getPinOption({ collapsed: false, pinned: false }, vi.fn())
|
||||
)
|
||||
expect(pin).toMatchObject({
|
||||
label: 'contextMenu.Pin',
|
||||
icon: 'icon-[lucide--pin]'
|
||||
})
|
||||
})
|
||||
|
||||
it('builds shape and color submenus and applies selected values', () => {
|
||||
customization.shapeOptions = [{ localizedName: 'Box', value: 'box' }]
|
||||
customization.colorOptions = [
|
||||
{
|
||||
name: 'noColor',
|
||||
localizedName: 'No Color',
|
||||
value: { dark: '#000', light: '#fff' }
|
||||
},
|
||||
{
|
||||
name: 'red',
|
||||
localizedName: 'Red',
|
||||
value: { dark: '#111', light: '#eee' }
|
||||
}
|
||||
]
|
||||
|
||||
const { visualOptions, colorSubmenu } = readNodeMenuOptions((options) => ({
|
||||
visualOptions: options.getNodeVisualOptions(
|
||||
{ collapsed: false, pinned: false },
|
||||
vi.fn()
|
||||
),
|
||||
colorSubmenu: options.colorSubmenu.value
|
||||
}))
|
||||
|
||||
expect(visualOptions[1].submenu).toEqual([
|
||||
expect.objectContaining({ label: 'Box' })
|
||||
])
|
||||
visualOptions[1].submenu?.[0].action()
|
||||
expect(customization.applyShape).toHaveBeenCalledWith(
|
||||
customization.shapeOptions[0]
|
||||
)
|
||||
|
||||
expect(colorSubmenu).toEqual([
|
||||
expect.objectContaining({ label: 'No Color', color: '#000' }),
|
||||
expect.objectContaining({ label: 'Red', color: '#111' })
|
||||
])
|
||||
colorSubmenu[0].action()
|
||||
colorSubmenu[1].action()
|
||||
expect(customization.applyColor).toHaveBeenNthCalledWith(1, null)
|
||||
expect(customization.applyColor).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
customization.colorOptions[1]
|
||||
)
|
||||
})
|
||||
|
||||
it('uses light-theme colors for the color submenu', () => {
|
||||
customization.isLightTheme.value = true
|
||||
customization.colorOptions = [
|
||||
{
|
||||
name: 'red',
|
||||
localizedName: 'Red',
|
||||
value: { dark: '#111', light: '#eee' }
|
||||
}
|
||||
]
|
||||
|
||||
expect(
|
||||
readNodeMenuOptions((options) => options.colorSubmenu.value[0].color)
|
||||
).toBe('#eee')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,34 +4,45 @@ import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
publishSubgraph: vi.fn(),
|
||||
selectedItems: [] as unknown[]
|
||||
selectedItems: [] as unknown[],
|
||||
getSelectedNodes: vi.fn((): unknown[] => []),
|
||||
getCanvas: vi.fn(),
|
||||
updateSelectedItems: vi.fn(),
|
||||
revokeSubgraphPreviews: vi.fn(),
|
||||
activeWorkflow: null as null | {
|
||||
changeTracker?: {
|
||||
captureCanvasState: () => void
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/canvas/useSelectedLiteGraphItems', () => ({
|
||||
useSelectedLiteGraphItems: () => ({
|
||||
getSelectedNodes: vi.fn(() => [])
|
||||
getSelectedNodes: mocks.getSelectedNodes
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
getCanvas: vi.fn(),
|
||||
getCanvas: mocks.getCanvas,
|
||||
get selectedItems() {
|
||||
return mocks.selectedItems
|
||||
},
|
||||
updateSelectedItems: vi.fn()
|
||||
updateSelectedItems: mocks.updateSelectedItems
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: null
|
||||
get activeWorkflow() {
|
||||
return mocks.activeWorkflow
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/nodeOutputStore', () => ({
|
||||
useNodeOutputStore: () => ({
|
||||
revokeSubgraphPreviews: vi.fn()
|
||||
revokeSubgraphPreviews: mocks.revokeSubgraphPreviews
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -50,10 +61,36 @@ function createRegularNode(): LGraphNode {
|
||||
return new LGraphNode('testnode')
|
||||
}
|
||||
|
||||
function createCanvas({
|
||||
graph,
|
||||
subgraph,
|
||||
selectedItems = []
|
||||
}: {
|
||||
graph?: {
|
||||
convertToSubgraph?: ReturnType<typeof vi.fn>
|
||||
unpackSubgraph?: ReturnType<typeof vi.fn>
|
||||
}
|
||||
subgraph?: {
|
||||
convertToSubgraph?: ReturnType<typeof vi.fn>
|
||||
unpackSubgraph?: ReturnType<typeof vi.fn>
|
||||
}
|
||||
selectedItems?: unknown[]
|
||||
} = {}) {
|
||||
return {
|
||||
graph,
|
||||
subgraph,
|
||||
selectedItems: new Set(selectedItems),
|
||||
select: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
describe('useSubgraphOperations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.selectedItems = []
|
||||
mocks.getSelectedNodes.mockReturnValue([])
|
||||
mocks.getCanvas.mockReturnValue(createCanvas())
|
||||
mocks.activeWorkflow = null
|
||||
})
|
||||
|
||||
it('addSubgraphToLibrary calls publishSubgraph when single SubgraphNode selected', async () => {
|
||||
@@ -103,4 +140,126 @@ describe('useSubgraphOperations', () => {
|
||||
|
||||
expect(mocks.publishSubgraph).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports selected subgraph and selectable node state', async () => {
|
||||
mocks.selectedItems = [createRegularNode()]
|
||||
mocks.getSelectedNodes.mockReturnValue([])
|
||||
|
||||
const { useSubgraphOperations } =
|
||||
await import('@/composables/graph/useSubgraphOperations')
|
||||
const { isSubgraphSelected, hasSelectableNodes } = useSubgraphOperations()
|
||||
|
||||
expect(isSubgraphSelected()).toBe(false)
|
||||
expect(hasSelectableNodes()).toBe(false)
|
||||
|
||||
mocks.selectedItems = [createSubgraphNode()]
|
||||
mocks.getSelectedNodes.mockReturnValue([createRegularNode()])
|
||||
|
||||
expect(isSubgraphSelected()).toBe(true)
|
||||
expect(hasSelectableNodes()).toBe(true)
|
||||
})
|
||||
|
||||
it('converts selected items to a subgraph and captures workflow state', async () => {
|
||||
const captureCanvasState = vi.fn()
|
||||
const node = createSubgraphNode()
|
||||
const graph = {
|
||||
convertToSubgraph: vi.fn(() => ({ node })),
|
||||
unpackSubgraph: vi.fn()
|
||||
}
|
||||
const canvas = createCanvas({
|
||||
graph,
|
||||
selectedItems: [createRegularNode()]
|
||||
})
|
||||
mocks.getCanvas.mockReturnValue(canvas)
|
||||
mocks.activeWorkflow = {
|
||||
changeTracker: {
|
||||
captureCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
const { useSubgraphOperations } =
|
||||
await import('@/composables/graph/useSubgraphOperations')
|
||||
const { convertToSubgraph } = useSubgraphOperations()
|
||||
|
||||
convertToSubgraph()
|
||||
|
||||
expect(graph.convertToSubgraph).toHaveBeenCalledWith(canvas.selectedItems)
|
||||
expect(canvas.select).toHaveBeenCalledWith(node)
|
||||
expect(mocks.updateSelectedItems).toHaveBeenCalledOnce()
|
||||
expect(captureCanvasState).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not select or capture when conversion has no graph or no result', async () => {
|
||||
const graph = {
|
||||
convertToSubgraph: vi.fn(() => null),
|
||||
unpackSubgraph: vi.fn()
|
||||
}
|
||||
const canvas = createCanvas({ graph })
|
||||
mocks.getCanvas
|
||||
.mockReturnValueOnce(createCanvas())
|
||||
.mockReturnValueOnce(canvas)
|
||||
|
||||
const { useSubgraphOperations } =
|
||||
await import('@/composables/graph/useSubgraphOperations')
|
||||
const { convertToSubgraph } = useSubgraphOperations()
|
||||
|
||||
expect(convertToSubgraph()).toBeNull()
|
||||
expect(convertToSubgraph()).toBeUndefined()
|
||||
expect(canvas.select).not.toHaveBeenCalled()
|
||||
expect(mocks.updateSelectedItems).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('unpacks selected subgraph nodes from the active graph and revokes previews', async () => {
|
||||
const captureCanvasState = vi.fn()
|
||||
const subgraphNode = createSubgraphNode()
|
||||
const graph = {
|
||||
convertToSubgraph: vi.fn(),
|
||||
unpackSubgraph: vi.fn()
|
||||
}
|
||||
mocks.getCanvas.mockReturnValue(
|
||||
createCanvas({
|
||||
subgraph: graph,
|
||||
selectedItems: [subgraphNode, createRegularNode()]
|
||||
})
|
||||
)
|
||||
mocks.activeWorkflow = {
|
||||
changeTracker: {
|
||||
captureCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
const { useSubgraphOperations } =
|
||||
await import('@/composables/graph/useSubgraphOperations')
|
||||
const { unpackSubgraph } = useSubgraphOperations()
|
||||
|
||||
unpackSubgraph()
|
||||
|
||||
expect(mocks.revokeSubgraphPreviews).toHaveBeenCalledWith(subgraphNode)
|
||||
expect(graph.unpackSubgraph).toHaveBeenCalledWith(subgraphNode, {
|
||||
skipMissingNodes: true
|
||||
})
|
||||
expect(captureCanvasState).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not unpack when no graph or no subgraph nodes are selected', async () => {
|
||||
const graph = {
|
||||
convertToSubgraph: vi.fn(),
|
||||
unpackSubgraph: vi.fn()
|
||||
}
|
||||
mocks.getCanvas
|
||||
.mockReturnValueOnce(createCanvas())
|
||||
.mockReturnValueOnce(
|
||||
createCanvas({ graph, selectedItems: [createRegularNode()] })
|
||||
)
|
||||
|
||||
const { useSubgraphOperations } =
|
||||
await import('@/composables/graph/useSubgraphOperations')
|
||||
const { unpackSubgraph } = useSubgraphOperations()
|
||||
|
||||
unpackSubgraph()
|
||||
unpackSubgraph()
|
||||
|
||||
expect(graph.unpackSubgraph).not.toHaveBeenCalled()
|
||||
expect(mocks.revokeSubgraphPreviews).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ResultItem } from '@/schemas/apiSchema'
|
||||
import type { ResultItem, ResultItemType } from '@/schemas/apiSchema'
|
||||
|
||||
const { mockFetchApi, mockAddAlert, mockUpdateInputs } = vi.hoisted(() => ({
|
||||
mockFetchApi: vi.fn(),
|
||||
@@ -11,22 +11,41 @@ const { mockFetchApi, mockAddAlert, mockUpdateInputs } = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
let capturedDragOnDrop: (files: File[]) => Promise<string[]>
|
||||
let capturedResultItemDrop: (item: ResultItem) => void
|
||||
let capturedPasteOnPaste: (files: File[]) => Promise<string[]>
|
||||
let capturedFileInputOnSelect: (files: File[]) => Promise<string[]>
|
||||
const mockOpenFileSelection = vi.fn()
|
||||
|
||||
vi.mock('@/composables/node/useNodeDragAndDrop', () => ({
|
||||
useNodeDragAndDrop: (
|
||||
_node: LGraphNode,
|
||||
opts: { onDrop: typeof capturedDragOnDrop }
|
||||
opts: {
|
||||
onDrop: typeof capturedDragOnDrop
|
||||
onResultItemDrop: typeof capturedResultItemDrop
|
||||
}
|
||||
) => {
|
||||
capturedDragOnDrop = opts.onDrop
|
||||
capturedResultItemDrop = opts.onResultItemDrop
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/node/useNodeFileInput', () => ({
|
||||
useNodeFileInput: () => ({ openFileSelection: vi.fn() })
|
||||
useNodeFileInput: (
|
||||
_node: LGraphNode,
|
||||
opts: { onSelect: typeof capturedFileInputOnSelect }
|
||||
) => {
|
||||
capturedFileInputOnSelect = opts.onSelect
|
||||
return { openFileSelection: mockOpenFileSelection }
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/node/useNodePaste', () => ({
|
||||
useNodePaste: vi.fn()
|
||||
useNodePaste: (
|
||||
_node: LGraphNode,
|
||||
opts: { onPaste: typeof capturedPasteOnPaste }
|
||||
) => {
|
||||
capturedPasteOnPaste = opts.onPaste
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
@@ -46,12 +65,14 @@ vi.mock('@/stores/assetsStore', () => ({
|
||||
}))
|
||||
|
||||
function createMockNode(): LGraphNode {
|
||||
return fromAny<LGraphNode, unknown>({
|
||||
const node = fromPartial<LGraphNode>({
|
||||
isUploading: false,
|
||||
imgs: [new Image()],
|
||||
graph: { setDirtyCanvas: vi.fn() },
|
||||
size: [300, 400]
|
||||
size: [300, 400],
|
||||
constructor: {}
|
||||
})
|
||||
node.imgs = [new Image()]
|
||||
return node
|
||||
}
|
||||
|
||||
function createFile(name = 'test.png', type = 'image/png'): File {
|
||||
@@ -78,6 +99,26 @@ describe('useNodeImageUpload', () => {
|
||||
let onUploadStart: (files: File[]) => void
|
||||
let onUploadError: () => void
|
||||
|
||||
async function mountImageUpload(
|
||||
options: { folder?: ResultItemType } = { folder: 'input' }
|
||||
) {
|
||||
const { useNodeImageUpload } = await import('./useNodeImageUpload')
|
||||
return useNodeImageUpload(node, {
|
||||
onUploadComplete,
|
||||
onUploadStart,
|
||||
onUploadError,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
function lastUploadBody() {
|
||||
const body = mockFetchApi.mock.calls.at(-1)?.[1]?.body
|
||||
if (!(body instanceof FormData)) {
|
||||
throw new Error('Expected upload body to be FormData')
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
@@ -86,13 +127,7 @@ describe('useNodeImageUpload', () => {
|
||||
onUploadStart = vi.fn()
|
||||
onUploadError = vi.fn()
|
||||
|
||||
const { useNodeImageUpload } = await import('./useNodeImageUpload')
|
||||
useNodeImageUpload(node, {
|
||||
onUploadComplete,
|
||||
onUploadStart,
|
||||
onUploadError,
|
||||
folder: 'input'
|
||||
})
|
||||
await mountImageUpload()
|
||||
})
|
||||
|
||||
it.for([
|
||||
@@ -180,4 +215,60 @@ describe('useNodeImageUpload', () => {
|
||||
await capturedDragOnDrop([createFile()])
|
||||
expect(node.graph?.setDirtyCanvas).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('passes dropped result items through without uploading', () => {
|
||||
const resultItem = fromPartial<ResultItem>({
|
||||
filename: 'existing.png',
|
||||
subfolder: '',
|
||||
type: 'input'
|
||||
})
|
||||
|
||||
capturedResultItemDrop(resultItem)
|
||||
|
||||
expect(onUploadComplete).toHaveBeenCalledWith([resultItem])
|
||||
expect(mockFetchApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uploads pasted images to the pasted subfolder', async () => {
|
||||
const { handleUpload } = await mountImageUpload({})
|
||||
mockFetchApi.mockResolvedValueOnce(successResponse('image.png'))
|
||||
|
||||
await handleUpload(createFile('image.png'))
|
||||
|
||||
const body = lastUploadBody()
|
||||
expect(body.get('subfolder')).toBe('pasted')
|
||||
expect(body.get('type')).toBeNull()
|
||||
expect(mockUpdateInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refreshes input assets for default non-pasted uploads', async () => {
|
||||
const { handleUpload } = await mountImageUpload({})
|
||||
mockFetchApi.mockResolvedValueOnce(successResponse('upload.png'))
|
||||
|
||||
await handleUpload(createFile('upload.png'))
|
||||
|
||||
const body = lastUploadBody()
|
||||
expect(body.get('subfolder')).toBeNull()
|
||||
expect(body.get('type')).toBeNull()
|
||||
expect(mockUpdateInputs).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not refresh input assets for explicit output uploads', async () => {
|
||||
await mountImageUpload({ folder: 'output' })
|
||||
mockFetchApi.mockResolvedValueOnce(successResponse('output.png'))
|
||||
|
||||
await capturedFileInputOnSelect([createFile('output.png')])
|
||||
|
||||
const body = lastUploadBody()
|
||||
expect(body.get('type')).toBe('output')
|
||||
expect(mockUpdateInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a specific alert for upload timeouts', async () => {
|
||||
mockFetchApi.mockRejectedValueOnce(new DOMException('', 'TimeoutError'))
|
||||
|
||||
await capturedPasteOnPaste([createFile()])
|
||||
|
||||
expect(mockAddAlert).toHaveBeenCalledWith('g.uploadTimedOut')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { StrokeProcessor } from '@/composables/maskeditor/StrokeProcessor'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { createMockCanvas2DContext } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
import { usePainter } from './usePainter'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: vi.fn(() => ({
|
||||
t: (key: string, params?: Record<string, unknown>) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key
|
||||
}))
|
||||
}))
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
painter: {
|
||||
uploadError: 'Failed to upload painter image: {status} - {statusText}'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useElementSize: vi.fn(() => ({
|
||||
@@ -27,10 +37,12 @@ vi.mock('@vueuse/core', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/maskeditor/StrokeProcessor', () => ({
|
||||
StrokeProcessor: vi.fn(() => ({
|
||||
addPoint: vi.fn(() => []),
|
||||
endStroke: vi.fn(() => [])
|
||||
}))
|
||||
StrokeProcessor: vi.fn(function StrokeProcessor() {
|
||||
return {
|
||||
addPoint: vi.fn(() => []),
|
||||
endStroke: vi.fn(() => [])
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
@@ -42,14 +54,15 @@ vi.mock('@/platform/updates/common/toastStore', () => {
|
||||
return { useToastStore: () => store }
|
||||
})
|
||||
|
||||
vi.mock('@/stores/nodeOutputStore', () => {
|
||||
const store = {
|
||||
getNodeImageUrls: vi.fn(() => undefined),
|
||||
nodeOutputs: {},
|
||||
nodePreviewImages: {}
|
||||
}
|
||||
return { useNodeOutputStore: () => store }
|
||||
})
|
||||
const mockNodeOutputStore = vi.hoisted(() => ({
|
||||
getNodeImageUrls: vi.fn(() => undefined as string[] | undefined),
|
||||
nodeOutputs: {},
|
||||
nodePreviewImages: {}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/nodeOutputStore', () => ({
|
||||
useNodeOutputStore: () => mockNodeOutputStore
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
@@ -61,7 +74,7 @@ vi.mock('@/scripts/api', () => ({
|
||||
const mockWidgets: IBaseWidget[] = []
|
||||
const mockProperties: Record<string, unknown> = {}
|
||||
const mockIsInputConnected = vi.fn(() => false)
|
||||
const mockGetInputNode = vi.fn(() => null)
|
||||
const mockGetInputNode = vi.fn((): LGraphNode | null => null)
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
@@ -84,18 +97,18 @@ vi.mock('@/scripts/app', () => ({
|
||||
|
||||
type PainterResult = ReturnType<typeof usePainter>
|
||||
|
||||
function makeWidget(name: string, value: unknown = null): IBaseWidget {
|
||||
return {
|
||||
function makeWidget(
|
||||
name: string,
|
||||
value: boolean | number | string | object | undefined = undefined
|
||||
): IBaseWidget {
|
||||
return fromPartial<IBaseWidget>({
|
||||
name,
|
||||
value,
|
||||
callback: vi.fn(),
|
||||
serializeValue: undefined
|
||||
} as unknown as IBaseWidget
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts a thin wrapper component so Vue lifecycle hooks fire.
|
||||
*/
|
||||
function mountPainter(
|
||||
nodeId: NodeId = toNodeId('test-node'),
|
||||
initialModelValue = ''
|
||||
@@ -119,11 +132,128 @@ function mountPainter(
|
||||
}
|
||||
})
|
||||
|
||||
render(Wrapper)
|
||||
return { painter, canvasEl, cursorEl, modelValue }
|
||||
const rendered = render(Wrapper, { global: { plugins: [i18n] } })
|
||||
return { painter, canvasEl, cursorEl, modelValue, unmount: rendered.unmount }
|
||||
}
|
||||
|
||||
function createCanvasContext() {
|
||||
const gradient = { addColorStop: vi.fn() }
|
||||
return createMockCanvas2DContext({
|
||||
createRadialGradient: vi.fn(() => gradient),
|
||||
drawImage: vi.fn(),
|
||||
globalCompositeOperation: '' as GlobalCompositeOperation,
|
||||
globalAlpha: 1,
|
||||
lineCap: 'butt',
|
||||
lineJoin: 'miter'
|
||||
})
|
||||
}
|
||||
|
||||
function createCanvasElement(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width = 100,
|
||||
height = 100
|
||||
) {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
vi.spyOn(canvas, 'getContext').mockReturnValue(fromPartial(ctx))
|
||||
vi.spyOn(canvas, 'getBoundingClientRect').mockReturnValue({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width,
|
||||
height,
|
||||
right: width,
|
||||
bottom: height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: vi.fn()
|
||||
})
|
||||
vi.spyOn(canvas, 'toBlob').mockImplementation((cb) => cb(new Blob(['x'])))
|
||||
return canvas
|
||||
}
|
||||
|
||||
async function mountPainterWithMaskCanvas({
|
||||
modelValue = '',
|
||||
toBlob = (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
}: {
|
||||
modelValue?: string
|
||||
toBlob?: (cb: BlobCallback) => void
|
||||
} = {}) {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
|
||||
const fakeCanvas = fromPartial<HTMLCanvasElement>({
|
||||
width: 4,
|
||||
height: 4,
|
||||
getContext: vi.fn(() => ({ clearRect: vi.fn() })),
|
||||
toBlob
|
||||
})
|
||||
|
||||
const mounted = mountPainter(toNodeId('test-node'), modelValue)
|
||||
mounted.canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
|
||||
return { maskWidget, ...mounted }
|
||||
}
|
||||
|
||||
function stubFakeImage() {
|
||||
const images: Array<{
|
||||
onload: (() => void) | null
|
||||
onerror: (() => void) | null
|
||||
}> = []
|
||||
class FakeImage {
|
||||
crossOrigin = ''
|
||||
naturalWidth = 64
|
||||
naturalHeight = 32
|
||||
onload: (() => void) | null = null
|
||||
onerror: (() => void) | null = null
|
||||
src = ''
|
||||
|
||||
constructor() {
|
||||
images.push(this)
|
||||
}
|
||||
}
|
||||
vi.stubGlobal('Image', FakeImage)
|
||||
return images
|
||||
}
|
||||
|
||||
function createPointerEvent(
|
||||
type: string,
|
||||
values: {
|
||||
clientX?: number
|
||||
clientY?: number
|
||||
offsetX?: number
|
||||
offsetY?: number
|
||||
button?: number
|
||||
pointerId?: number
|
||||
target?: Pick<HTMLElement, 'setPointerCapture' | 'releasePointerCapture'>
|
||||
} = {}
|
||||
) {
|
||||
const event = new PointerEvent(type, {
|
||||
button: values.button ?? 0,
|
||||
clientX: values.clientX ?? 0,
|
||||
clientY: values.clientY ?? 0,
|
||||
pointerId: values.pointerId ?? 1
|
||||
})
|
||||
Object.defineProperty(event, 'offsetX', { value: values.offsetX ?? 0 })
|
||||
Object.defineProperty(event, 'offsetY', { value: values.offsetY ?? 0 })
|
||||
Object.defineProperty(event, 'target', {
|
||||
value:
|
||||
values.target ??
|
||||
({
|
||||
setPointerCapture: vi.fn(),
|
||||
releasePointerCapture: vi.fn()
|
||||
} as Pick<HTMLElement, 'setPointerCapture' | 'releasePointerCapture'>)
|
||||
})
|
||||
return event
|
||||
}
|
||||
|
||||
describe('usePainter', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.resetAllMocks()
|
||||
@@ -133,6 +263,7 @@ describe('usePainter', () => {
|
||||
}
|
||||
mockIsInputConnected.mockReturnValue(false)
|
||||
mockGetInputNode.mockReturnValue(null)
|
||||
mockNodeOutputStore.getNodeImageUrls.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
describe('syncCanvasSizeFromWidgets', () => {
|
||||
@@ -151,6 +282,25 @@ describe('usePainter', () => {
|
||||
expect(painter.canvasWidth.value).toBe(512)
|
||||
expect(painter.canvasHeight.value).toBe(512)
|
||||
})
|
||||
|
||||
it('keeps defaults when the node id is empty', async () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
|
||||
const { painter } = mountPainter(toNodeId(''))
|
||||
|
||||
expect(app.canvas.graph!.getNodeById).not.toHaveBeenCalled()
|
||||
expect(painter.canvasWidth.value).toBe(512)
|
||||
expect(painter.canvasHeight.value).toBe(512)
|
||||
expect(painter.inputImageUrl.value).toBeNull()
|
||||
expect(painter.isImageInputConnected.value).toBe(false)
|
||||
expect(maskWidget.serializeValue).toBeUndefined()
|
||||
|
||||
painter.brushSize.value = 36
|
||||
await nextTick()
|
||||
|
||||
expect(mockProperties.painterBrushSize).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreSettingsFromProperties', () => {
|
||||
@@ -226,6 +376,18 @@ describe('usePainter', () => {
|
||||
expect(widthWidget.callback).toHaveBeenCalledWith(800)
|
||||
expect(heightWidget.callback).toHaveBeenCalledWith(600)
|
||||
})
|
||||
|
||||
it('skips widget callbacks when dimensions are unchanged', async () => {
|
||||
const widthWidget = makeWidget('width', 512)
|
||||
const heightWidget = makeWidget('height', 512)
|
||||
mockWidgets.push(widthWidget, heightWidget)
|
||||
|
||||
mountPainter()
|
||||
await nextTick()
|
||||
|
||||
expect(widthWidget.callback).not.toHaveBeenCalled()
|
||||
expect(heightWidget.callback).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncBackgroundColorToWidget', () => {
|
||||
@@ -241,6 +403,16 @@ describe('usePainter', () => {
|
||||
expect(bgWidget.value).toBe('#ff00ff')
|
||||
expect(bgWidget.callback).toHaveBeenCalledWith('#ff00ff')
|
||||
})
|
||||
|
||||
it('skips widget callbacks when the background color is unchanged', async () => {
|
||||
const bgWidget = makeWidget('bg_color', '#000000')
|
||||
mockWidgets.push(bgWidget)
|
||||
|
||||
mountPainter()
|
||||
await nextTick()
|
||||
|
||||
expect(bgWidget.callback).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateInputImageUrl', () => {
|
||||
@@ -258,6 +430,34 @@ describe('usePainter', () => {
|
||||
|
||||
expect(painter.isImageInputConnected.value).toBe(true)
|
||||
})
|
||||
|
||||
it('sets inputImageUrl from the connected input node output', () => {
|
||||
const inputNode = {} as LGraphNode
|
||||
mockIsInputConnected.mockReturnValue(true)
|
||||
mockGetInputNode.mockReturnValue(inputNode)
|
||||
mockNodeOutputStore.getNodeImageUrls.mockReturnValue([
|
||||
'http://localhost:8188/view?filename=input.png'
|
||||
])
|
||||
|
||||
const { painter } = mountPainter()
|
||||
|
||||
expect(mockNodeOutputStore.getNodeImageUrls).toHaveBeenCalledWith(
|
||||
inputNode
|
||||
)
|
||||
expect(painter.inputImageUrl.value).toBe(
|
||||
'http://localhost:8188/view?filename=input.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps inputImageUrl null when a connected input has no images', () => {
|
||||
mockIsInputConnected.mockReturnValue(true)
|
||||
mockGetInputNode.mockReturnValue({} as LGraphNode)
|
||||
mockNodeOutputStore.getNodeImageUrls.mockReturnValue([])
|
||||
|
||||
const { painter } = mountPainter()
|
||||
|
||||
expect(painter.inputImageUrl.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleInputImageLoad', () => {
|
||||
@@ -268,12 +468,9 @@ describe('usePainter', () => {
|
||||
|
||||
const { painter } = mountPainter()
|
||||
|
||||
const fakeEvent = {
|
||||
target: {
|
||||
naturalWidth: 1920,
|
||||
naturalHeight: 1080
|
||||
}
|
||||
} as unknown as Event
|
||||
const fakeEvent = fromPartial<Event>({
|
||||
target: { naturalWidth: 1920, naturalHeight: 1080 } as HTMLImageElement
|
||||
})
|
||||
|
||||
painter.handleInputImageLoad(fakeEvent)
|
||||
|
||||
@@ -282,6 +479,19 @@ describe('usePainter', () => {
|
||||
expect(widthWidget.value).toBe(1920)
|
||||
expect(heightWidget.value).toBe(1080)
|
||||
})
|
||||
|
||||
it('updates canvas size when dimension widgets are absent', () => {
|
||||
const { painter } = mountPainter()
|
||||
|
||||
painter.handleInputImageLoad(
|
||||
fromPartial<Event>({
|
||||
target: { naturalWidth: 320, naturalHeight: 240 } as HTMLImageElement
|
||||
})
|
||||
)
|
||||
|
||||
expect(painter.canvasWidth.value).toBe(320)
|
||||
expect(painter.canvasHeight.value).toBe(240)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cursor visibility', () => {
|
||||
@@ -299,6 +509,17 @@ describe('usePainter', () => {
|
||||
painter.handlePointerLeave()
|
||||
expect(painter.cursorVisible.value).toBe(false)
|
||||
})
|
||||
|
||||
it('positions the custom cursor on pointer movement', () => {
|
||||
const { painter, cursorEl } = mountPainter()
|
||||
cursorEl.value = document.createElement('div')
|
||||
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { offsetX: 25, offsetY: 30 })
|
||||
)
|
||||
|
||||
expect(cursorEl.value.style.transform).toBe('translate(15px, 20px)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('displayBrushSize', () => {
|
||||
@@ -365,24 +586,12 @@ describe('usePainter', () => {
|
||||
})
|
||||
|
||||
it('uploads the current canvas when no cached modelValue is present, even if nothing has been painted yet', async () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
|
||||
const fetchApiMock = vi.mocked(api.fetchApi)
|
||||
fetchApiMock.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
json: async () => ({ name: 'uploaded.png' })
|
||||
} as Response)
|
||||
fetchApiMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ name: 'uploaded.png' }))
|
||||
)
|
||||
|
||||
const fakeCanvas = {
|
||||
width: 4,
|
||||
height: 4,
|
||||
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
|
||||
canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
expect(fetchApiMock).toHaveBeenCalledWith(
|
||||
@@ -399,53 +608,137 @@ describe('usePainter', () => {
|
||||
})
|
||||
|
||||
it('throws when the upload response is missing a name', async () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({}))
|
||||
)
|
||||
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce({
|
||||
status: 200,
|
||||
json: async () => ({})
|
||||
} as Response)
|
||||
|
||||
const fakeCanvas = {
|
||||
width: 4,
|
||||
height: 4,
|
||||
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
|
||||
canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/missing 'name'/)
|
||||
})
|
||||
|
||||
it('throws when the upload response body is not valid JSON', async () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
it('throws when the upload request fails', async () => {
|
||||
vi.mocked(api.fetchApi).mockRejectedValueOnce(new Error('offline'))
|
||||
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce({
|
||||
status: 200,
|
||||
json: async () => {
|
||||
throw new SyntaxError('Unexpected token')
|
||||
}
|
||||
} as unknown as Response)
|
||||
|
||||
const fakeCanvas = {
|
||||
width: 4,
|
||||
height: 4,
|
||||
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
|
||||
canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/painter\.uploadError/)
|
||||
).rejects.toThrow(/Failed to upload painter image/)
|
||||
})
|
||||
|
||||
it('reports non-error upload rejections', async () => {
|
||||
vi.mocked(api.fetchApi).mockRejectedValueOnce('offline')
|
||||
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/offline/)
|
||||
})
|
||||
|
||||
it('throws when the upload response is not successful', async () => {
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
new Response('upload failed', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error'
|
||||
})
|
||||
)
|
||||
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/upload failed/)
|
||||
})
|
||||
|
||||
it('uses statusText when an unsuccessful upload response has no body', async () => {
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
new Response('', { status: 502, statusText: 'Bad Gateway' })
|
||||
)
|
||||
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/Bad Gateway/)
|
||||
})
|
||||
|
||||
it('uses unknown error when an unsuccessful upload response has no detail', async () => {
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
new Response('', { status: 500, statusText: '' })
|
||||
)
|
||||
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/unknown error/)
|
||||
})
|
||||
|
||||
it('throws when the upload response body is not valid JSON', async () => {
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
new Response('not valid json{')
|
||||
)
|
||||
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/Failed to upload painter image/)
|
||||
})
|
||||
|
||||
it('reports non-error JSON parse failures', async () => {
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
fromPartial<Response>({
|
||||
status: 200,
|
||||
json: async () => {
|
||||
throw 'bad json'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const { maskWidget } = await mountPainterWithMaskCanvas()
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).rejects.toThrow(/bad json/)
|
||||
})
|
||||
|
||||
it('returns modelValue when dirty canvas serialization produces no blob', async () => {
|
||||
const { painter, maskWidget } = await mountPainterWithMaskCanvas({
|
||||
toBlob: (cb) => cb(null)
|
||||
})
|
||||
|
||||
painter.handleClear()
|
||||
await nextTick()
|
||||
|
||||
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
|
||||
expect(result).toBe('')
|
||||
expect(api.fetchApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns existing modelValue when canvas serialization produces no blob', async () => {
|
||||
const toBlob = vi.fn((cb: BlobCallback) => cb(null))
|
||||
const { painter, maskWidget, modelValue } =
|
||||
await mountPainterWithMaskCanvas({
|
||||
modelValue: 'painter/cached.png [temp]',
|
||||
toBlob
|
||||
})
|
||||
|
||||
// handleClear marks the canvas dirty; restore the cached value it wipes
|
||||
painter.handleClear()
|
||||
modelValue.value = 'painter/cached.png [temp]'
|
||||
await nextTick()
|
||||
|
||||
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
|
||||
expect(toBlob).toHaveBeenCalled()
|
||||
expect(result).toBe('painter/cached.png [temp]')
|
||||
expect(api.fetchApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns existing modelValue when canvas element is unmounted at serialize time', async () => {
|
||||
@@ -462,13 +755,13 @@ describe('usePainter', () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
|
||||
const fakeCanvas = {
|
||||
const fakeCanvas = fromPartial<HTMLCanvasElement>({
|
||||
width: 4,
|
||||
height: 4,
|
||||
getContext: vi.fn(() => ({
|
||||
clearRect: vi.fn()
|
||||
}))
|
||||
} as unknown as HTMLCanvasElement
|
||||
})
|
||||
|
||||
const { painter, canvasEl, modelValue } = mountPainter(
|
||||
toNodeId('test-node'),
|
||||
@@ -498,6 +791,83 @@ describe('usePainter', () => {
|
||||
expect.stringContaining('type=temp')
|
||||
)
|
||||
})
|
||||
|
||||
it('defaults restored mask type to input when no type suffix exists', () => {
|
||||
vi.mocked(api.apiURL).mockClear()
|
||||
|
||||
mountPainter(toNodeId('test-node'), 'plain.png')
|
||||
|
||||
expect(api.apiURL).toHaveBeenCalledWith(
|
||||
expect.stringContaining('filename=plain.png')
|
||||
)
|
||||
expect(api.apiURL).toHaveBeenCalledWith(
|
||||
expect.stringContaining('type=input')
|
||||
)
|
||||
expect(api.apiURL).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('subfolder=')
|
||||
)
|
||||
})
|
||||
|
||||
it('does not restore a canvas when the mask value is blank', () => {
|
||||
vi.mocked(api.apiURL).mockClear()
|
||||
|
||||
mountPainter(toNodeId('test-node'), ' ')
|
||||
|
||||
expect(api.apiURL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('draws a restored mask after the image loads', () => {
|
||||
const images = stubFakeImage()
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
|
||||
const { painter, canvasEl } = mountPainter(
|
||||
toNodeId('test-node'),
|
||||
'painter/mask.png [temp]'
|
||||
)
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
images[0].onload?.()
|
||||
|
||||
expect(painter.canvasWidth.value).toBe(64)
|
||||
expect(painter.canvasHeight.value).toBe(32)
|
||||
expect(ctx.drawImage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores restored image loads after the canvas unmounts', () => {
|
||||
const images = stubFakeImage()
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
|
||||
const { painter, canvasEl, unmount } = mountPainter(
|
||||
toNodeId('test-node'),
|
||||
'painter/mask.png [temp]'
|
||||
)
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
unmount()
|
||||
// Vue clears template refs on unmount
|
||||
canvasEl.value = null
|
||||
images[0].onload?.()
|
||||
|
||||
expect(painter.canvasWidth.value).toBe(512)
|
||||
expect(painter.canvasHeight.value).toBe(512)
|
||||
expect(ctx.drawImage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears stale modelValue when restored image loading fails', () => {
|
||||
const images = stubFakeImage()
|
||||
|
||||
const { modelValue } = mountPainter(
|
||||
toNodeId('test-node'),
|
||||
'painter/mask.png [temp]'
|
||||
)
|
||||
images[0].onerror?.()
|
||||
|
||||
expect(modelValue.value).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleClear', () => {
|
||||
@@ -506,6 +876,35 @@ describe('usePainter', () => {
|
||||
|
||||
expect(() => painter.handleClear()).not.toThrow()
|
||||
})
|
||||
|
||||
it('clears the canvas and marks the current mask dirty', async () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
const ctx = createCanvasContext()
|
||||
mockWidgets.push(maskWidget)
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
|
||||
const { painter, canvasEl, modelValue } = mountPainter(
|
||||
toNodeId('test-node'),
|
||||
'painter/cached.png [temp]'
|
||||
)
|
||||
canvasEl.value = createCanvasElement(ctx, 50, 40)
|
||||
|
||||
painter.handleClear()
|
||||
await nextTick()
|
||||
|
||||
expect(ctx.clearRect).toHaveBeenCalledWith(0, 0, 50, 40)
|
||||
expect(modelValue.value).toBe('')
|
||||
|
||||
vi.mocked(api.fetchApi).mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ name: 'cleared.png' }))
|
||||
)
|
||||
|
||||
await expect(
|
||||
maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
).resolves.toBe('cleared.png [input]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handlePointerDown', () => {
|
||||
@@ -547,6 +946,184 @@ describe('usePainter', () => {
|
||||
|
||||
expect(() => painter.handlePointerDown(event)).not.toThrow()
|
||||
})
|
||||
|
||||
it('draws a hard brush stroke across pointer events', () => {
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', {
|
||||
clientX: 60,
|
||||
clientY: 10,
|
||||
offsetX: 60,
|
||||
offsetY: 10
|
||||
})
|
||||
)
|
||||
painter.handlePointerUp(createPointerEvent('pointerup'))
|
||||
|
||||
expect(ctx.arc).toHaveBeenCalled()
|
||||
expect(ctx.moveTo).toHaveBeenCalled()
|
||||
expect(ctx.lineTo).toHaveBeenCalled()
|
||||
expect(ctx.stroke).toHaveBeenCalled()
|
||||
expect(ctx.drawImage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('draws a soft brush stroke with radial dabs', () => {
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
painter.brushHardness.value = 0.5
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { clientX: 70, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerUp(createPointerEvent('pointerup'))
|
||||
|
||||
expect(ctx.createRadialGradient).toHaveBeenCalled()
|
||||
expect(ctx.arc).toHaveBeenCalled()
|
||||
expect(ctx.drawImage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses destination-out composition for eraser strokes', () => {
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
painter.tool.value = 'eraser'
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
|
||||
expect(ctx.globalCompositeOperation).toBe('destination-out')
|
||||
})
|
||||
|
||||
it('does not start drawing when a canvas context is unavailable', () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
vi.spyOn(canvas, 'getContext').mockReturnValue(null)
|
||||
vi.spyOn(canvas, 'getBoundingClientRect').mockReturnValue({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
right: 100,
|
||||
bottom: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: vi.fn()
|
||||
})
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = canvas
|
||||
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
try {
|
||||
expect(() => {
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { clientX: 20, clientY: 20 })
|
||||
)
|
||||
painter.handlePointerUp(createPointerEvent('pointerup'))
|
||||
}).not.toThrow()
|
||||
|
||||
expect(canvas.getContext).toHaveBeenCalledWith('2d')
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
errorSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('uses one animation frame for pending pointer movement', () => {
|
||||
const ctx = createCanvasContext()
|
||||
let frameCallback: FrameRequestCallback | undefined
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
|
||||
(callback) => {
|
||||
frameCallback = callback
|
||||
return 7
|
||||
}
|
||||
)
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { clientX: 20, clientY: 20 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { clientX: 30, clientY: 30 })
|
||||
)
|
||||
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1)
|
||||
|
||||
frameCallback?.(0)
|
||||
|
||||
expect(ctx.lineTo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('flushes a pending pointer movement when leaving the canvas', () => {
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockReturnValue(7)
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { clientX: 20, clientY: 20 })
|
||||
)
|
||||
painter.handlePointerLeave()
|
||||
|
||||
expect(window.cancelAnimationFrame).toHaveBeenCalledWith(7)
|
||||
expect(ctx.lineTo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cancels a pending pointer movement when unmounted', () => {
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockReturnValue(7)
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
const { painter, canvasEl, unmount } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerMove(
|
||||
createPointerEvent('pointermove', { clientX: 20, clientY: 20 })
|
||||
)
|
||||
unmount()
|
||||
|
||||
expect(window.cancelAnimationFrame).toHaveBeenCalledWith(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handlePointerUp', () => {
|
||||
@@ -554,12 +1131,13 @@ describe('usePainter', () => {
|
||||
const { painter } = mountPainter()
|
||||
|
||||
const mockReleasePointerCapture = vi.fn()
|
||||
const event = {
|
||||
const event = createPointerEvent('pointerup', {
|
||||
button: 2,
|
||||
target: {
|
||||
setPointerCapture: vi.fn(),
|
||||
releasePointerCapture: mockReleasePointerCapture
|
||||
}
|
||||
} as unknown as PointerEvent
|
||||
})
|
||||
|
||||
painter.handlePointerUp(event)
|
||||
|
||||
@@ -569,17 +1147,48 @@ describe('usePainter', () => {
|
||||
it('tolerates releasePointerCapture throwing for synthetic events', () => {
|
||||
const { painter } = mountPainter()
|
||||
|
||||
const event = {
|
||||
const event = createPointerEvent('pointerup', {
|
||||
button: 0,
|
||||
pointerId: 1,
|
||||
target: {
|
||||
setPointerCapture: vi.fn(),
|
||||
releasePointerCapture: vi.fn(() => {
|
||||
throw new DOMException('NotFoundError')
|
||||
})
|
||||
}
|
||||
} as unknown as PointerEvent
|
||||
})
|
||||
|
||||
expect(() => painter.handlePointerUp(event)).not.toThrow()
|
||||
})
|
||||
|
||||
it('draws final stroke processor points on release', () => {
|
||||
const ctx = createCanvasContext()
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
fromPartial(ctx)
|
||||
)
|
||||
vi.mocked(StrokeProcessor).mockImplementationOnce(
|
||||
class extends StrokeProcessor {
|
||||
constructor() {
|
||||
super(1)
|
||||
}
|
||||
override addPoint = vi.fn(() => [])
|
||||
override endStroke = vi.fn(() => [
|
||||
{ x: 40, y: 10 },
|
||||
{ x: 80, y: 10 }
|
||||
])
|
||||
}
|
||||
)
|
||||
const { painter, canvasEl } = mountPainter()
|
||||
canvasEl.value = createCanvasElement(ctx)
|
||||
|
||||
painter.handlePointerDown(
|
||||
createPointerEvent('pointerdown', { clientX: 10, clientY: 10 })
|
||||
)
|
||||
painter.handlePointerUp(createPointerEvent('pointerup'))
|
||||
|
||||
expect(ctx.moveTo).toHaveBeenCalledWith(10, 10)
|
||||
expect(ctx.lineTo).toHaveBeenCalledWith(40, 10)
|
||||
expect(ctx.lineTo).toHaveBeenCalledWith(80, 10)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,58 @@
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { ref } from 'vue'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useWaveAudioPlayer } from './useWaveAudioPlayer'
|
||||
|
||||
type MediaControls = {
|
||||
playing: Ref<boolean>
|
||||
currentTime: Ref<number>
|
||||
duration: Ref<number>
|
||||
volume: Ref<number>
|
||||
muted: Ref<boolean>
|
||||
}
|
||||
|
||||
const mockMediaControls = vi.hoisted(() => ({
|
||||
values: [] as MediaControls[]
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>()
|
||||
return {
|
||||
...actual,
|
||||
useMediaControls: () => ({
|
||||
playing: ref(false),
|
||||
currentTime: ref(0),
|
||||
duration: ref(0)
|
||||
})
|
||||
useMediaControls: () =>
|
||||
mockMediaControls.values.shift() ?? {
|
||||
playing: ref(false),
|
||||
currentTime: ref(0),
|
||||
duration: ref(0),
|
||||
volume: ref(1),
|
||||
muted: ref(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mockFetchApi = vi.fn()
|
||||
const originalAudioContext = globalThis.AudioContext
|
||||
|
||||
function queueMediaControls(overrides: Partial<MediaControls> = {}) {
|
||||
const controls: MediaControls = {
|
||||
playing: ref(false),
|
||||
currentTime: ref(0),
|
||||
duration: ref(0),
|
||||
volume: ref(1),
|
||||
muted: ref(false),
|
||||
...overrides
|
||||
}
|
||||
mockMediaControls.values.push(controls)
|
||||
return controls
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockMediaControls.values = []
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.AudioContext = originalAudioContext
|
||||
vi.unstubAllGlobals()
|
||||
mockFetchApi.mockReset()
|
||||
})
|
||||
|
||||
@@ -50,6 +82,21 @@ describe('useWaveAudioPlayer', () => {
|
||||
expect(playedBarIndex.value).toBe(-1)
|
||||
})
|
||||
|
||||
it('computes progress and played bar when duration is known', () => {
|
||||
queueMediaControls({
|
||||
currentTime: ref(30),
|
||||
duration: ref(120)
|
||||
})
|
||||
const src = ref('')
|
||||
const { playedBarIndex, progressRatio } = useWaveAudioPlayer({
|
||||
src,
|
||||
barCount: 40
|
||||
})
|
||||
|
||||
expect(playedBarIndex.value).toBe(9)
|
||||
expect(progressRatio.value).toBe(25)
|
||||
})
|
||||
|
||||
it('generates bars with heights between 10 and 70', () => {
|
||||
const src = ref('')
|
||||
const { bars } = useWaveAudioPlayer({ src })
|
||||
@@ -65,6 +112,56 @@ describe('useWaveAudioPlayer', () => {
|
||||
expect(isPlaying.value).toBe(false)
|
||||
})
|
||||
|
||||
it('updates playback and seek controls', () => {
|
||||
const controls = queueMediaControls({
|
||||
currentTime: ref(10),
|
||||
duration: ref(100)
|
||||
})
|
||||
const src = ref('')
|
||||
const player = useWaveAudioPlayer({ src })
|
||||
|
||||
player.togglePlayPause()
|
||||
expect(player.isPlaying.value).toBe(true)
|
||||
|
||||
player.seekToStart()
|
||||
expect(controls.currentTime.value).toBe(0)
|
||||
|
||||
player.seekToRatio(0.25)
|
||||
expect(controls.currentTime.value).toBe(25)
|
||||
|
||||
player.seekToRatio(-1)
|
||||
expect(controls.currentTime.value).toBe(0)
|
||||
|
||||
player.seekToRatio(2)
|
||||
expect(controls.currentTime.value).toBe(100)
|
||||
|
||||
player.seekToEnd()
|
||||
expect(controls.currentTime.value).toBe(100)
|
||||
expect(player.isPlaying.value).toBe(false)
|
||||
})
|
||||
|
||||
it('updates mute state and volume icon', () => {
|
||||
const controls = queueMediaControls({
|
||||
volume: ref(1),
|
||||
muted: ref(false)
|
||||
})
|
||||
const src = ref('')
|
||||
const player = useWaveAudioPlayer({ src })
|
||||
|
||||
expect(player.volumeIcon.value).toBe('icon-[lucide--volume-2]')
|
||||
|
||||
controls.volume.value = 0.25
|
||||
expect(player.volumeIcon.value).toBe('icon-[lucide--volume-1]')
|
||||
|
||||
controls.volume.value = 0
|
||||
expect(player.volumeIcon.value).toBe('icon-[lucide--volume-x]')
|
||||
|
||||
controls.volume.value = 1
|
||||
player.toggleMute()
|
||||
expect(controls.muted.value).toBe(true)
|
||||
expect(player.volumeIcon.value).toBe('icon-[lucide--volume-x]')
|
||||
})
|
||||
|
||||
it('shows 0:00 for formatted times initially', () => {
|
||||
const src = ref('')
|
||||
const { formattedCurrentTime, formattedDuration } = useWaveAudioPlayer({
|
||||
@@ -81,7 +178,8 @@ describe('useWaveAudioPlayer', () => {
|
||||
|
||||
const mockDecodeAudioData = vi.fn(() => Promise.resolve(mockAudioBuffer))
|
||||
const mockClose = vi.fn().mockResolvedValue(undefined)
|
||||
globalThis.AudioContext = fromAny<typeof AudioContext, unknown>(
|
||||
vi.stubGlobal(
|
||||
'AudioContext',
|
||||
class {
|
||||
decodeAudioData = mockDecodeAudioData
|
||||
close = mockClose
|
||||
@@ -108,6 +206,92 @@ describe('useWaveAudioPlayer', () => {
|
||||
expect(bars.value).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('uses placeholder bars when decoded audio has no channel data', async () => {
|
||||
const mockAudioBuffer = {
|
||||
getChannelData: vi.fn(() => new Float32Array())
|
||||
}
|
||||
const mockDecodeAudioData = vi.fn(() => Promise.resolve(mockAudioBuffer))
|
||||
const mockClose = vi.fn().mockResolvedValue(undefined)
|
||||
vi.stubGlobal(
|
||||
'AudioContext',
|
||||
class {
|
||||
decodeAudioData = mockDecodeAudioData
|
||||
close = mockClose
|
||||
}
|
||||
)
|
||||
mockFetchApi.mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8))
|
||||
})
|
||||
const src = ref('/api/view?filename=empty.wav&type=output')
|
||||
const { bars, loading } = useWaveAudioPlayer({ src, barCount: 6 })
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(loading.value).toBe(false)
|
||||
})
|
||||
|
||||
expect(bars.value).toHaveLength(6)
|
||||
for (const bar of bars.value) {
|
||||
expect(bar.height).toBeGreaterThanOrEqual(10)
|
||||
expect(bar.height).toBeLessThanOrEqual(70)
|
||||
}
|
||||
})
|
||||
|
||||
it('uses placeholder bars when fetching audio fails', async () => {
|
||||
mockFetchApi.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500
|
||||
})
|
||||
const src = ref('https://example.com/audio.wav')
|
||||
const { bars, loading } = useWaveAudioPlayer({ src, barCount: 5 })
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(loading.value).toBe(false)
|
||||
})
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('https://example.com/audio.wav')
|
||||
expect(bars.value).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('seeks from waveform clicks and starts playback', () => {
|
||||
const controls = queueMediaControls({
|
||||
duration: ref(100)
|
||||
})
|
||||
const src = ref('')
|
||||
const player = useWaveAudioPlayer({ src })
|
||||
|
||||
player.handleWaveformClick(fromPartial<MouseEvent>({ clientX: 50 }))
|
||||
expect(controls.currentTime.value).toBe(0)
|
||||
|
||||
player.waveformRef.value = fromPartial<HTMLElement>({
|
||||
getBoundingClientRect: () => ({ left: 10, width: 100 })
|
||||
})
|
||||
|
||||
player.handleWaveformClick(fromPartial<MouseEvent>({ clientX: 60 }))
|
||||
expect(controls.currentTime.value).toBe(50)
|
||||
expect(player.isPlaying.value).toBe(true)
|
||||
|
||||
player.handleWaveformClick(fromPartial<MouseEvent>({ clientX: -100 }))
|
||||
expect(controls.currentTime.value).toBe(0)
|
||||
|
||||
player.handleWaveformClick(fromPartial<MouseEvent>({ clientX: 999 }))
|
||||
expect(controls.currentTime.value).toBe(100)
|
||||
})
|
||||
|
||||
it('ignores waveform clicks when duration is zero', () => {
|
||||
const controls = queueMediaControls()
|
||||
const src = ref('')
|
||||
const player = useWaveAudioPlayer({ src })
|
||||
player.waveformRef.value = fromPartial<HTMLElement>({
|
||||
getBoundingClientRect: () => ({ left: 0, width: 100 })
|
||||
})
|
||||
|
||||
player.handleWaveformClick(fromPartial<MouseEvent>({ clientX: 50 }))
|
||||
|
||||
expect(controls.currentTime.value).toBe(0)
|
||||
expect(player.isPlaying.value).toBe(false)
|
||||
})
|
||||
|
||||
it('does not call decodeAudioSource when src is empty', () => {
|
||||
const src = ref('')
|
||||
useWaveAudioPlayer({ src })
|
||||
|
||||
@@ -51,6 +51,10 @@ export type OAuthConsentDecisionParams = {
|
||||
expectedRedirectUri?: string
|
||||
}
|
||||
|
||||
export type OAuthConsentDecision = (
|
||||
params: OAuthConsentDecisionParams
|
||||
) => Promise<void>
|
||||
|
||||
// Schemes that execute in our origin if navigated. Never navigable,
|
||||
// regardless of what the backend returns. Everything else is governed
|
||||
// by binding to the challenge's registered redirect_uri — no per-client
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import CreditSlider from './CreditSlider.vue'
|
||||
|
||||
const meta: Meta<typeof CreditSlider> = {
|
||||
title: 'Platform/Subscription/CreditSlider',
|
||||
component: CreditSlider,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
disabled: { control: 'boolean' }
|
||||
},
|
||||
args: {
|
||||
disabled: false
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
// Previews at the real layout width: the Figma "Team Plan" card column is
|
||||
// 512px wide with 32px padding (DES-197), i.e. a 448px content area — the
|
||||
// width the slider actually renders into inside PricingTableWorkspace.
|
||||
template: '<div class="w-[512px] px-8"><story /></div>'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
components: { CreditSlider },
|
||||
setup() {
|
||||
const value = ref(700)
|
||||
return { args, value }
|
||||
},
|
||||
template: '<CreditSlider v-model="value" :disabled="args.disabled" />'
|
||||
})
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { disabled: true },
|
||||
render: (args) => ({
|
||||
components: { CreditSlider },
|
||||
setup() {
|
||||
const value = ref(700)
|
||||
return { args, value }
|
||||
},
|
||||
template: '<CreditSlider v-model="value" :disabled="args.disabled" />'
|
||||
})
|
||||
}
|
||||
|
||||
// Sample `GET /api/billing/plans → team_credit_stops` payload (DES-197 yearly).
|
||||
// In production this comes from the API; here it shows the stops being driven
|
||||
// entirely through props rather than the hardcoded default constant.
|
||||
const apiTeamCreditStops = {
|
||||
default_stop_index: 2,
|
||||
stops: [
|
||||
{
|
||||
id: 'team_200',
|
||||
credits: 42_200,
|
||||
yearly: { price_cents: 20_000, discount_percent: 0 }
|
||||
},
|
||||
{
|
||||
id: 'team_400',
|
||||
credits: 84_400,
|
||||
yearly: { price_cents: 38_000, discount_percent: 5 }
|
||||
},
|
||||
{
|
||||
id: 'team_700',
|
||||
credits: 147_700,
|
||||
yearly: { price_cents: 63_000, discount_percent: 10 }
|
||||
},
|
||||
{
|
||||
id: 'team_1400',
|
||||
credits: 295_400,
|
||||
yearly: { price_cents: 119_000, discount_percent: 15 }
|
||||
},
|
||||
{
|
||||
id: 'team_2500',
|
||||
credits: 527_500,
|
||||
yearly: { price_cents: 200_000, discount_percent: 20 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Reference adapter (FE-934 will own this in the data layer): API → CreditStop[].
|
||||
// The pre-discount list price is recovered as discounted / (1 - discount).
|
||||
const mappedStops = apiTeamCreditStops.stops.map((s) => ({
|
||||
credits: s.credits,
|
||||
discountPercentYearly: s.yearly.discount_percent,
|
||||
usd: Math.round(
|
||||
s.yearly.price_cents / 100 / (1 - s.yearly.discount_percent / 100)
|
||||
)
|
||||
}))
|
||||
|
||||
export const BackendDrivenStops: Story = {
|
||||
name: 'Backend-driven stops (props)',
|
||||
render: (args) => ({
|
||||
components: { CreditSlider },
|
||||
setup() {
|
||||
const defaultStopIndex = apiTeamCreditStops.default_stop_index
|
||||
const value = ref(mappedStops[defaultStopIndex].usd)
|
||||
return { args, value, mappedStops, defaultStopIndex }
|
||||
},
|
||||
template:
|
||||
'<CreditSlider v-model="value" :stops="mappedStops" :default-stop-index="defaultStopIndex" :disabled="args.disabled" />'
|
||||
})
|
||||
}
|
||||
189
src/platform/cloud/subscription/components/CreditSlider.test.ts
Normal file
189
src/platform/cloud/subscription/components/CreditSlider.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { render, screen, within } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { usdToCredits } from '@/base/credits/comfyCredits'
|
||||
import { TEAM_PLAN_CREDIT_STOPS } from '@/platform/cloud/subscription/constants/teamPlanCreditStops'
|
||||
|
||||
import CreditSlider from './CreditSlider.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
subscription: {
|
||||
usdPerMonth: 'USD / mo',
|
||||
billedYearly: '{total} Billed yearly',
|
||||
creditSliderSave: 'Save {percent}% ({amount})'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function renderSlider(props: Record<string, unknown> = {}) {
|
||||
return render(CreditSlider, { props, global: { plugins: [i18n] } })
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
describe('CreditSlider', () => {
|
||||
it('defaults to the $700 stop (index 2) when no value is bound', async () => {
|
||||
renderSlider()
|
||||
await flush()
|
||||
|
||||
const thumb = screen.getByRole('slider')
|
||||
expect(thumb).toHaveAttribute('aria-valuemin', '0')
|
||||
expect(thumb).toHaveAttribute('aria-valuemax', '4')
|
||||
expect(thumb).toHaveAttribute('aria-valuenow', '2')
|
||||
})
|
||||
|
||||
it('snaps to the next fixed stop on ArrowRight (never a value in between)', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onUpdate = vi.fn<(usd: number) => void>()
|
||||
|
||||
renderSlider({ modelValue: 700, 'onUpdate:modelValue': onUpdate })
|
||||
await flush()
|
||||
|
||||
screen.getByRole('slider').focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith(1400)
|
||||
})
|
||||
|
||||
it('snaps to the previous fixed stop on ArrowLeft', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onUpdate = vi.fn<(usd: number) => void>()
|
||||
|
||||
renderSlider({ modelValue: 700, 'onUpdate:modelValue': onUpdate })
|
||||
await flush()
|
||||
|
||||
screen.getByRole('slider').focus()
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith(400)
|
||||
})
|
||||
|
||||
it('emits change with the full {index, usd, credits} payload', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
renderSlider({ modelValue: 700, onChange })
|
||||
await flush()
|
||||
|
||||
screen.getByRole('slider').focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
index: 3,
|
||||
usd: 1400,
|
||||
credits: 295_400
|
||||
})
|
||||
})
|
||||
|
||||
it('emits nothing when disabled (keyboard interaction suppressed)', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onUpdate = vi.fn<(usd: number) => void>()
|
||||
const onChange = vi.fn()
|
||||
|
||||
renderSlider({
|
||||
modelValue: 700,
|
||||
disabled: true,
|
||||
'onUpdate:modelValue': onUpdate,
|
||||
onChange
|
||||
})
|
||||
await flush()
|
||||
|
||||
screen.getByRole('slider').focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
expect(onUpdate).not.toHaveBeenCalled()
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows the discounted price, struck original, save badge and yearly total (DES-197)', async () => {
|
||||
renderSlider() // default $700 stop → 10% yearly discount
|
||||
await flush()
|
||||
|
||||
expect(screen.getByTestId('credit-slider-price')).toHaveTextContent('$630')
|
||||
expect(
|
||||
screen.getByTestId('credit-slider-original-price')
|
||||
).toHaveTextContent('$700')
|
||||
expect(screen.getByTestId('credit-slider-save')).toHaveTextContent(
|
||||
'Save 10% ($70)'
|
||||
)
|
||||
expect(screen.getByTestId('credit-slider-billed-yearly')).toHaveTextContent(
|
||||
'$7,560'
|
||||
)
|
||||
})
|
||||
|
||||
it('hides the discount UI at the 0% stop ($200)', async () => {
|
||||
renderSlider({ modelValue: 200 })
|
||||
await flush()
|
||||
|
||||
expect(screen.getByTestId('credit-slider-price')).toHaveTextContent('$200')
|
||||
expect(
|
||||
screen.queryByTestId('credit-slider-original-price')
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('credit-slider-save')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders all five fixed credit stop labels', async () => {
|
||||
renderSlider({ modelValue: 700 })
|
||||
await flush()
|
||||
|
||||
const stops = within(screen.getByTestId('credit-slider-stops'))
|
||||
for (const label of ['42.2K', '84.4K', '147.7K', '295.4K', '527.5K']) {
|
||||
expect(stops.getByText(label)).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
it('renders nothing when stops is empty (defensive for BE-sourced data)', async () => {
|
||||
renderSlider({ stops: [] })
|
||||
await flush()
|
||||
|
||||
expect(screen.queryByRole('slider')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('credit-slider-price')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders stops + default index supplied via props (BE-sourced override)', async () => {
|
||||
const stops = [
|
||||
{ usd: 50, credits: 10_550, discountPercentYearly: 0 },
|
||||
{ usd: 100, credits: 21_100, discountPercentYearly: 25 }
|
||||
]
|
||||
// No modelValue → the model default ($700) matches no stop, so selectedIndex
|
||||
// falls back to defaultStopIndex (here index 1 → $100).
|
||||
renderSlider({ stops, defaultStopIndex: 1 })
|
||||
await flush()
|
||||
|
||||
const thumb = screen.getByRole('slider')
|
||||
expect(thumb).toHaveAttribute('aria-valuemax', '1') // 2 stops → max index 1
|
||||
expect(thumb).toHaveAttribute('aria-valuenow', '1') // default index honored
|
||||
|
||||
// index 1 → $100 at 25% yearly → $75 discounted, struck $100, save $25
|
||||
expect(screen.getByTestId('credit-slider-price')).toHaveTextContent('$75')
|
||||
expect(
|
||||
screen.getByTestId('credit-slider-original-price')
|
||||
).toHaveTextContent('$100')
|
||||
expect(screen.getByTestId('credit-slider-save')).toHaveTextContent(
|
||||
'Save 25% ($25)'
|
||||
)
|
||||
|
||||
// Only the prop's labels render — none of the DES-197 defaults.
|
||||
const labels = within(screen.getByTestId('credit-slider-stops'))
|
||||
expect(labels.getByText('10.6K')).toBeInTheDocument()
|
||||
expect(labels.getByText('21.1K')).toBeInTheDocument()
|
||||
expect(labels.queryByText('147.7K')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps every credit amount equal to usdToCredits(usd) (guards rate drift)', () => {
|
||||
for (const stop of TEAM_PLAN_CREDIT_STOPS) {
|
||||
expect(stop.credits).toBe(usdToCredits(stop.usd))
|
||||
}
|
||||
})
|
||||
})
|
||||
228
src/platform/cloud/subscription/components/CreditSlider.vue
Normal file
228
src/platform/cloud/subscription/components/CreditSlider.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
TransitionPresets,
|
||||
usePreferredReducedMotion,
|
||||
useTransition
|
||||
} from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
import {
|
||||
DEFAULT_TEAM_PLAN_STOP_INDEX,
|
||||
TEAM_PLAN_CREDIT_STOPS
|
||||
} from '@/platform/cloud/subscription/constants/teamPlanCreditStops'
|
||||
import type { CreditStop } from '@/platform/cloud/subscription/constants/teamPlanCreditStops'
|
||||
|
||||
const {
|
||||
disabled = false,
|
||||
class: rootClass,
|
||||
stops = TEAM_PLAN_CREDIT_STOPS,
|
||||
defaultStopIndex = DEFAULT_TEAM_PLAN_STOP_INDEX
|
||||
} = defineProps<{
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
/**
|
||||
* The fixed credit stops the slider snaps to; when empty, the component
|
||||
* renders nothing. Defaults to the hardcoded DES-197 set; pass the
|
||||
* backend-sourced stops once the contract lands — map
|
||||
* `GET /api/billing/plans → team_credit_stops.stops` to `CreditStop[]`
|
||||
* (credits, the pre-discount `usd`, and `discountPercentYearly`).
|
||||
*/
|
||||
stops?: readonly CreditStop[]
|
||||
/**
|
||||
* Stop selected when the bound value matches none (e.g. first render).
|
||||
* Maps to `team_credit_stops.default_stop_index`. Defaults to DES-197 ($700).
|
||||
*/
|
||||
defaultStopIndex?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** Fired when the selected stop changes, with the full derived payload. */
|
||||
change: [stop: { index: number; usd: number; credits: number }]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* v-model carries the selected USD value (one of the `stops`). The literal
|
||||
* default keeps `defineModel` statically analyzable; when custom `stops` are
|
||||
* passed without a matching v-model, `selectedIndex` falls back to
|
||||
* `defaultStopIndex`, so the displayed stop is still correct.
|
||||
*/
|
||||
const usd = defineModel<number>({
|
||||
default: TEAM_PLAN_CREDIT_STOPS[DEFAULT_TEAM_PLAN_STOP_INDEX].usd
|
||||
})
|
||||
|
||||
const selectedIndex = computed(() => {
|
||||
const i = stops.findIndex((stop) => stop.usd === usd.value)
|
||||
if (i !== -1) return i
|
||||
// Fall back to the default stop, clamped into range: a backend-driven `stops`
|
||||
// array can be shorter than expected (or `defaultStopIndex` out of bounds).
|
||||
return Math.min(Math.max(defaultStopIndex, 0), Math.max(stops.length - 1, 0))
|
||||
})
|
||||
|
||||
// Zero-stop fallback: `useTransition` reads its source eagerly at setup, so an
|
||||
// empty `stops` must not crash even though the template then renders nothing.
|
||||
const EMPTY_STOP: CreditStop = { usd: 0, credits: 0, discountPercentYearly: 0 }
|
||||
const current = computed(() => stops.at(selectedIndex.value) ?? EMPTY_STOP)
|
||||
|
||||
// Yearly commitment (per DES-197): the discount applies to the monthly figure.
|
||||
// The card shows the discounted monthly price, the struck pre-discount price,
|
||||
// the saving, and the annual total.
|
||||
const discountedMonthly = computed(() =>
|
||||
Math.round(
|
||||
current.value.usd * (1 - current.value.discountPercentYearly / 100)
|
||||
)
|
||||
)
|
||||
const saveAmount = computed(() => current.value.usd - discountedMonthly.value)
|
||||
const hasDiscount = computed(() => current.value.discountPercentYearly > 0)
|
||||
|
||||
/**
|
||||
* Smoothly count the price figures up/down as the slider moves between stops
|
||||
* instead of snapping. Honors the user's reduced-motion preference. The save
|
||||
* badge ("X% ($Y)") is intentionally left snapping — its percent is a discrete
|
||||
* tier, so animating the bracketed amount alone would read inconsistently.
|
||||
*/
|
||||
const prefersReducedMotion = usePreferredReducedMotion()
|
||||
const priceTween = {
|
||||
duration: 350,
|
||||
easing: TransitionPresets.easeOutCubic,
|
||||
disabled: computed(() => prefersReducedMotion.value === 'reduce')
|
||||
}
|
||||
// One vector tween keeps both figures in phase. Deriving the monthly from the
|
||||
// animated original instead would jump at the start of each move: the discount
|
||||
// tier snaps per stop while the base price is still mid-tween.
|
||||
const animatedPrices = useTransition(
|
||||
() => [discountedMonthly.value, current.value.usd],
|
||||
priceTween
|
||||
)
|
||||
|
||||
const displayMonthly = computed(() => Math.round(animatedPrices.value[0]))
|
||||
const displayOriginal = computed(() => Math.round(animatedPrices.value[1]))
|
||||
// Derive the yearly total from the displayed monthly so it always reads as
|
||||
// exactly 12× the price shown — even mid-count — rather than drifting as a
|
||||
// second, independently-phased tween would.
|
||||
const displayBilledYearly = computed(() => displayMonthly.value * 12)
|
||||
|
||||
/**
|
||||
* Bridge the discrete stop index (0..n-1) to the reka-ui slider's `number[]`
|
||||
* model. Driving the slider in index space with `step = 1` guarantees the
|
||||
* thumb can only land on the fixed stops — never a value in between.
|
||||
*/
|
||||
const sliderModel = computed<number[]>({
|
||||
get: () => [selectedIndex.value],
|
||||
set: ([index]) => {
|
||||
const stop = stops[index]
|
||||
if (!stop) return
|
||||
usd.value = stop.usd
|
||||
emit('change', { index, usd: stop.usd, credits: stop.credits })
|
||||
}
|
||||
})
|
||||
|
||||
const lastIndex = computed(() => Math.max(stops.length - 1, 0))
|
||||
|
||||
const formatUsd = (value: number) => `$${value.toLocaleString('en-US')}`
|
||||
const formatCreditsCompact = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
}).format(value)
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="stops.length > 0"
|
||||
:class="cn('flex w-full flex-col gap-3', rootClass)"
|
||||
>
|
||||
<!-- Price: discounted monthly + struck pre-discount + save badge -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span class="flex shrink-0 items-baseline gap-1.5 whitespace-nowrap">
|
||||
<span
|
||||
class="text-[2rem]/none font-semibold text-base-foreground"
|
||||
data-testid="credit-slider-price"
|
||||
>
|
||||
{{ formatUsd(displayMonthly) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hasDiscount"
|
||||
class="text-base text-muted-foreground line-through"
|
||||
data-testid="credit-slider-original-price"
|
||||
>
|
||||
{{ formatUsd(displayOriginal) }}
|
||||
</span>
|
||||
<span class="text-base text-muted-foreground">
|
||||
{{ t('subscription.usdPerMonth') }}
|
||||
</span>
|
||||
</span>
|
||||
<!-- Save badge: outlined primary pill, pushed to the right (DES-197) -->
|
||||
<span
|
||||
v-if="hasDiscount"
|
||||
data-testid="credit-slider-save"
|
||||
class="ms-auto shrink-0 rounded-full border-2 border-primary-background px-2 py-1 text-sm font-bold whitespace-nowrap text-primary-background"
|
||||
>
|
||||
{{
|
||||
t('subscription.creditSliderSave', {
|
||||
percent: current.discountPercentYearly,
|
||||
amount: formatUsd(saveAmount)
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="m-0 text-sm text-muted-foreground"
|
||||
data-testid="credit-slider-billed-yearly"
|
||||
>
|
||||
{{
|
||||
t('subscription.billedYearly', {
|
||||
total: formatUsd(displayBilledYearly)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Discrete slider: snaps to the 5 fixed DES-197 stops -->
|
||||
<Slider
|
||||
v-model="sliderModel"
|
||||
:min="0"
|
||||
:max="lastIndex"
|
||||
:step="1"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
|
||||
<!-- Credit stop labels; the selected stop is emphasized -->
|
||||
<ol
|
||||
data-testid="credit-slider-stops"
|
||||
class="m-0 flex list-none justify-between p-0"
|
||||
>
|
||||
<li
|
||||
v-for="(stop, i) in stops"
|
||||
:key="stop.usd"
|
||||
:data-selected="i === selectedIndex ? '' : undefined"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-1 text-xs',
|
||||
i === selectedIndex
|
||||
? 'font-semibold text-base-foreground'
|
||||
: 'text-muted-foreground'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
'icon-[comfy--credits] size-3 shrink-0',
|
||||
i === selectedIndex ? 'bg-amber-400' : 'bg-muted-foreground'
|
||||
)
|
||||
"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ formatCreditsCompact(stop.credits) }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
@@ -308,6 +308,31 @@ export function findSubgraphPathById(
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the root parent node associated with a hierarchical execution ID.
|
||||
* Both Group Nodes and Subgraph Nodes use hierarchical IDs (e.g. "rootId:childId:...").
|
||||
* The root parent is always located in the rootGraph.
|
||||
*
|
||||
* @param rootGraph - The root graph to search from
|
||||
* @param executionId - The execution ID (e.g., "123:456")
|
||||
* @returns The root parent node if found, null otherwise
|
||||
*/
|
||||
export function getRootParentNode(
|
||||
rootGraph: LGraph,
|
||||
executionId: string
|
||||
): LGraphNode | null {
|
||||
const parts = parseExecutionId(executionId)
|
||||
if (!parts || parts.length < 2) return null
|
||||
|
||||
const parentId = parts[0]
|
||||
if (!rootGraph) return null
|
||||
|
||||
const localParentId = parseNodeId(parentId)
|
||||
if (!localParentId) return null
|
||||
|
||||
return rootGraph.getNodeById(localParentId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node by its execution ID from anywhere in the graph hierarchy.
|
||||
* Execution IDs use hierarchical format like "123:456:789" for nested nodes.
|
||||
|
||||
@@ -18,7 +18,7 @@ const MEDIA_SRC_REGEX =
|
||||
/(<(?:img|source|video)[^>]*\ssrc=['"])(?!(?:\/|https?:\/\/))([^'"\s>]+)(['"])/gi
|
||||
|
||||
// Create a marked Renderer that prefixes relative URLs with base
|
||||
function createMarkdownRenderer(baseUrl?: string): Renderer {
|
||||
export function createMarkdownRenderer(baseUrl?: string): Renderer {
|
||||
const normalizedBase = baseUrl ? baseUrl.replace(/\/+$/, '') : ''
|
||||
const renderer = new Renderer()
|
||||
renderer.image = ({ href, title, text }) => {
|
||||
|
||||
Reference in New Issue
Block a user