Compare commits
189 Commits
enable-non
...
backport-9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794f370990 | ||
|
|
b79fbe09c3 | ||
|
|
365378d10c | ||
|
|
d706f0315f | ||
|
|
1e62e5ccf1 | ||
|
|
aa4f23d90f | ||
|
|
964002cd2a | ||
|
|
130e1eed7a | ||
|
|
fdda951516 | ||
|
|
23f26efd94 | ||
|
|
afbdea2956 | ||
|
|
c46a95bd90 | ||
|
|
58460ada48 | ||
|
|
9ba3d75c5b | ||
|
|
c7cbefc256 | ||
|
|
4daba09415 | ||
|
|
b6ca126eff | ||
|
|
d4f6a9af0e | ||
|
|
b654d7c06a | ||
|
|
d3b67511f9 | ||
|
|
92a193203d | ||
|
|
25880aa024 | ||
|
|
40aa7c5974 | ||
|
|
3d3a4dd1a2 | ||
|
|
39af93ae3e | ||
|
|
4849d4a6c9 | ||
|
|
55ee6e7e63 | ||
|
|
74d285bda9 | ||
|
|
1bcebd8293 | ||
|
|
dac58ad811 | ||
|
|
6ee3803770 | ||
|
|
fd2ffb7100 | ||
|
|
c05644045f | ||
|
|
5fe902358c | ||
|
|
c1a569211d | ||
|
|
2b69d7b49c | ||
|
|
ee0789e153 | ||
|
|
c1d07d6424 | ||
|
|
4e9e3a0c26 | ||
|
|
f7a83f6dfa | ||
|
|
4103379901 | ||
|
|
337e0486ea | ||
|
|
b27eb5861a | ||
|
|
b3aed9afd0 | ||
|
|
7baa14af86 | ||
|
|
7921c38db9 | ||
|
|
46c40c755e | ||
|
|
c2452c5d20 | ||
|
|
7fba000d68 | ||
|
|
306fb94cf5 | ||
|
|
7bf9d51d1d | ||
|
|
01f59afff2 | ||
|
|
f4ca285d07 | ||
|
|
06732b84bb | ||
|
|
c809ac5a43 | ||
|
|
0792d26f77 | ||
|
|
03f597a496 | ||
|
|
473713cf02 | ||
|
|
541ad387b9 | ||
|
|
7feaefd39c | ||
|
|
73e4ae2f70 | ||
|
|
f5c9c72234 | ||
|
|
a1c54ad7aa | ||
|
|
6902e38e6a | ||
|
|
8f5cdead73 | ||
|
|
0cfd1d8e1f | ||
|
|
102149fc04 | ||
|
|
7c4486ed29 | ||
|
|
116685595b | ||
|
|
8744d3dd54 | ||
|
|
6c205cbf4c | ||
|
|
351d43a95a | ||
|
|
9dc6203b3d | ||
|
|
18875fb5e7 | ||
|
|
397af47035 | ||
|
|
44733f010d | ||
|
|
fe78bc6043 | ||
|
|
25696ffe03 | ||
|
|
3b5c9762a4 | ||
|
|
7060133ff9 | ||
|
|
cc2c10745b | ||
|
|
8ab9a7b887 | ||
|
|
2dbd7e86c3 | ||
|
|
faede75bb4 | ||
|
|
8099cce232 | ||
|
|
27d4a34435 | ||
|
|
e1e560403e | ||
|
|
aff0ebad50 | ||
|
|
44dc208339 | ||
|
|
388c21a88d | ||
|
|
b28f46d237 | ||
|
|
2900e5e52e | ||
|
|
07e64a7f44 | ||
|
|
34e21f3267 | ||
|
|
1349fffbce | ||
|
|
cde872fcf7 | ||
|
|
596df0f0c6 | ||
|
|
d3c0e331eb | ||
|
|
b47414a52f | ||
|
|
631d484901 | ||
|
|
e83e396c09 | ||
|
|
821c1e74ff | ||
|
|
d06cc0819a | ||
|
|
f5f5a77435 | ||
|
|
efe78b799f | ||
|
|
e70484d596 | ||
|
|
3dba245dd3 | ||
|
|
2ca0c30cf7 | ||
|
|
c8ba5f7300 | ||
|
|
39cc8ab97a | ||
|
|
2ee0a1337c | ||
|
|
980f280b3c | ||
|
|
4856fb0802 | ||
|
|
82ace36982 | ||
|
|
3d88d0a6ab | ||
|
|
21cfd44a2d | ||
|
|
d8d0dcbf71 | ||
|
|
066a1f1f11 | ||
|
|
2b896a722b | ||
|
|
96b9e886ea | ||
|
|
58182ddda7 | ||
|
|
0f0029ca29 | ||
|
|
ba7f622fbd | ||
|
|
fcb4341c98 | ||
|
|
27da781029 | ||
|
|
36d59f26cd | ||
|
|
5f7a6e7aba | ||
|
|
2c07bedbb1 | ||
|
|
78635294ce | ||
|
|
2f09c6321e | ||
|
|
38edba7024 | ||
|
|
f851c3189f | ||
|
|
71d26eb4d9 | ||
|
|
d04dd32235 | ||
|
|
c52f48af45 | ||
|
|
01cf3244b8 | ||
|
|
0f33444eef | ||
|
|
44ce9379eb | ||
|
|
138fa6a2ce | ||
|
|
ce9d0ca670 | ||
|
|
6cf0357b3e | ||
|
|
c0c81dba49 | ||
|
|
553ea63357 | ||
|
|
995ebc4ba4 | ||
|
|
d282353370 | ||
|
|
85ae0a57c3 | ||
|
|
0d64d503ec | ||
|
|
30ef6f2b8c | ||
|
|
6012341fd1 | ||
|
|
a80f6d7922 | ||
|
|
0f5aca6726 | ||
|
|
4fc1d2ef5b | ||
|
|
92b7437d86 | ||
|
|
dd1fefe843 | ||
|
|
adcb663b3e | ||
|
|
28b171168a | ||
|
|
69062c6da1 | ||
|
|
a7c2115166 | ||
|
|
d044bed9b2 | ||
|
|
d873c8048f | ||
|
|
475d7035f7 | ||
|
|
eb6bf91e20 | ||
|
|
422227d2fc | ||
|
|
10e9bc2f8d | ||
|
|
f7b835e6a5 | ||
|
|
7f30d6b6a5 | ||
|
|
da56c9e554 | ||
|
|
79063edf54 | ||
|
|
d4c40f5255 | ||
|
|
1e1d5c8308 | ||
|
|
e411a104f4 | ||
|
|
19a724710c | ||
|
|
9ecbb3af27 | ||
|
|
581452d312 | ||
|
|
9dde4e7bc7 | ||
|
|
0288ea5b39 | ||
|
|
061e96e488 | ||
|
|
ff9642d0cb | ||
|
|
a6620a4ddc | ||
|
|
9209badd37 | ||
|
|
815be49112 | ||
|
|
adbfb83767 | ||
|
|
3238ad3d32 | ||
|
|
be515d6fcc | ||
|
|
b583c92c64 | ||
|
|
c0a209226d | ||
|
|
c91d811d00 | ||
|
|
e625b0351c | ||
|
|
a56f2d3883 |
35
.github/workflows/ci-dist-telemetry-scan.yaml
vendored
@@ -34,10 +34,13 @@ jobs:
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
env:
|
||||
DISTRIBUTION: localhost
|
||||
|
||||
- name: Scan dist for telemetry references
|
||||
- name: Scan dist for GTM telemetry references
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo '🔍 Scanning for Google Tag Manager references...'
|
||||
if rg --no-ignore -n \
|
||||
-g '*.html' \
|
||||
-g '*.js' \
|
||||
@@ -46,7 +49,33 @@ jobs:
|
||||
-e '(?i)googletagmanager\.com/gtm\.js\\?id=' \
|
||||
-e '(?i)googletagmanager\.com/ns\.html\\?id=' \
|
||||
dist; then
|
||||
echo 'Telemetry references found in dist assets.'
|
||||
echo '❌ ERROR: Google Tag Manager references found in dist assets!'
|
||||
echo 'GTM must be properly tree-shaken from OSS builds.'
|
||||
exit 1
|
||||
fi
|
||||
echo 'No telemetry references found in dist assets.'
|
||||
echo '✅ No GTM references found'
|
||||
|
||||
- name: Scan dist for Mixpanel telemetry references
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo '🔍 Scanning for Mixpanel references...'
|
||||
if rg --no-ignore -n \
|
||||
-g '*.html' \
|
||||
-g '*.js' \
|
||||
-e '(?i)mixpanel\.init' \
|
||||
-e '(?i)mixpanel\.identify' \
|
||||
-e 'MixpanelTelemetryProvider' \
|
||||
-e 'mp\.comfy\.org' \
|
||||
-e 'mixpanel-browser' \
|
||||
-e '(?i)mixpanel\.track\(' \
|
||||
dist; then
|
||||
echo '❌ ERROR: Mixpanel references found in dist assets!'
|
||||
echo 'Mixpanel must be properly tree-shaken from OSS builds.'
|
||||
echo ''
|
||||
echo 'To fix this:'
|
||||
echo '1. Use the TelemetryProvider pattern (see src/platform/telemetry/)'
|
||||
echo '2. Call telemetry via useTelemetry() hook'
|
||||
echo '3. Use conditional dynamic imports behind isCloud checks'
|
||||
exit 1
|
||||
fi
|
||||
echo '✅ No Mixpanel references found'
|
||||
|
||||
14
.github/workflows/ci-lint-format.yaml
vendored
@@ -21,11 +21,19 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
token: ${{ !github.event.pull_request.head.repo.fork && secrets.PR_GH_TOKEN || github.token }}
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Detect browser_tests changes
|
||||
id: changed-paths
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
browser_tests:
|
||||
- 'browser_tests/**'
|
||||
|
||||
- name: Run ESLint with auto-fix
|
||||
run: pnpm lint:fix
|
||||
|
||||
@@ -60,6 +68,10 @@ jobs:
|
||||
pnpm format:check
|
||||
pnpm knip
|
||||
|
||||
- name: Typecheck browser tests
|
||||
if: steps.changed-paths.outputs.browser_tests == 'true'
|
||||
run: pnpm typecheck:browser
|
||||
|
||||
- name: Comment on PR about auto-fix
|
||||
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
continue-on-error: true
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"vue/no-import-compiler-macros": "error",
|
||||
"vue/no-dupe-keys": "error"
|
||||
},
|
||||
|
||||
@@ -98,12 +98,10 @@ const config: StorybookConfig = {
|
||||
},
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
treeshake: false,
|
||||
output: {
|
||||
keepNames: true
|
||||
keepNames: true,
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
|
||||
@@ -52,14 +52,15 @@
|
||||
"reference",
|
||||
"plugin",
|
||||
"custom-variant",
|
||||
"utility"
|
||||
"utility",
|
||||
"source"
|
||||
]
|
||||
}
|
||||
],
|
||||
"function-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreFunctions": ["theme", "v-bind"]
|
||||
"ignoreFunctions": ["theme", "v-bind", "from-folder", "from-json"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -37,6 +37,10 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
|
||||
|
||||
The project uses **Nx** for build orchestration and task management
|
||||
|
||||
## Package Manager
|
||||
|
||||
This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g., `pnpm test:unit`, `pnpm lint`). To run arbitrary packages not in scripts, use `pnpx` or `pnpm dlx` — never `npx`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `pnpm dev`: Start Vite dev server.
|
||||
|
||||
@@ -201,7 +201,7 @@ The project supports three types of icons, all with automatic imports (no manual
|
||||
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i class="icon-[lucide--settings]" />`, `<i class="icon-[mdi--folder]" />`
|
||||
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
|
||||
|
||||
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
|
||||
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Tailwind CSS icon classes (`icon-[comfy--template]`) are provided by `@iconify/tailwind4`, configured in `packages/design-system/src/css/style.css`. Custom icons are stored in `packages/design-system/src/icons/` and loaded via `from-folder` at build time.
|
||||
|
||||
For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).
|
||||
|
||||
|
||||
205
browser_tests/assets/missing/deprecated_nodes_complex.json
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"last_node_id": 7,
|
||||
"last_link_id": 5,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "T2IAdapterLoader",
|
||||
"pos": [100, 100],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONTROL_NET",
|
||||
"type": "CONTROL_NET",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "T2IAdapterLoader"
|
||||
},
|
||||
"widgets_values": ["t2iadapter_model.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [100, 300],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "ResizeImagesByLongerEdge",
|
||||
"pos": [500, 100],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [1],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ResizeImagesByLongerEdge"
|
||||
},
|
||||
"widgets_values": [1024]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "ImageScaleBy",
|
||||
"pos": [500, 280],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [2, 3],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageScaleBy"
|
||||
},
|
||||
"widgets_values": ["lanczos", 1.5]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "ImageBatch",
|
||||
"pos": [900, 100],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image1",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "image2",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [4],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageBatch"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SaveImage",
|
||||
"pos": [900, 300],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveImage"
|
||||
},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [1250, 100],
|
||||
"size": [300, 250],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 3, 0, 4, 0, "IMAGE"],
|
||||
[2, 4, 0, 5, 0, "IMAGE"],
|
||||
[3, 4, 0, 6, 0, "IMAGE"],
|
||||
[4, 5, 0, 7, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
186
browser_tests/assets/missing/deprecated_nodes_simple.json
Normal file
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Load3DAnimation",
|
||||
"pos": [100, 100],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MESH",
|
||||
"type": "MESH",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Load3DAnimation"
|
||||
},
|
||||
"widgets_values": ["model.glb"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Preview3DAnimation",
|
||||
"pos": [450, 100],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mesh",
|
||||
"type": "MESH",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Preview3DAnimation"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "ConditioningAverage ",
|
||||
"pos": [100, 300],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "conditioning_to",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "conditioning_from",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [1],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ConditioningAverage "
|
||||
},
|
||||
"widgets_values": [1]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "SDV_img2vid_Conditioning",
|
||||
"pos": [450, 300],
|
||||
"size": [300, 150],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip_vision",
|
||||
"type": "CLIP_VISION",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "init_image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "latent",
|
||||
"type": "LATENT",
|
||||
"links": [2],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SDV_img2vid_Conditioning"
|
||||
},
|
||||
"widgets_values": [1024, 576, 14, 127, 25, 0.02]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [800, 300],
|
||||
"size": [300, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [42, "fixed", 20, 8, "euler", "normal", 1]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 3, 0, 5, 1, "CONDITIONING"],
|
||||
[2, 4, 2, 5, 3, "LATENT"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
{
|
||||
"id": "9a37f747-e96b-4304-9212-7abcaad7bdac",
|
||||
"revision": 0,
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 5,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "1e38d8ea-45e1-48a5-aa20-966584201867",
|
||||
"pos": [788, 433.5],
|
||||
"size": [210, 108],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [5]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [["-1", "string_a"]]
|
||||
},
|
||||
"widgets_values": [""]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PreviewAny",
|
||||
"pos": [1135, 429],
|
||||
"size": [250, 145.5],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": [null, null, false]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [456, 450],
|
||||
"size": [225, 121.5],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [4]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Outer\n"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[4, 1, 0, 5, 0, "STRING"],
|
||||
[5, 5, 0, 2, 0, "STRING"]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "1e38d8ea-45e1-48a5-aa20-966584201867",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 6,
|
||||
"lastLinkId": 9,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [351, 432.5, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1315, 432.5, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "7bf3e1d4-0521-4b5c-92f5-47ca598b7eb4",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [1],
|
||||
"localized_name": "string_a",
|
||||
"pos": [451, 452.5]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "fbe975ba-d7c2-471e-a99a-a1e2c6ab466d",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [9],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1335, 452.5]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [815, 373],
|
||||
"size": [347, 231],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "9be42452-056b-4c99-9f9f-7381d11c4454",
|
||||
"pos": [955, 775],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [9]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [["-1", "string_a"]]
|
||||
},
|
||||
"widgets_values": [""]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [313, 685],
|
||||
"size": [325, 109],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 1\n"]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "9be42452-056b-4c99-9f9f-7381d11c4454",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 9,
|
||||
"lastLinkId": 12,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [680, 774, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1320, 774, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "01c05c51-86b5-4bad-b32f-9c911683a13d",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [4],
|
||||
"localized_name": "string_a",
|
||||
"pos": [780, 794]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "a8bcf3bf-a66a-4c71-8d92-17a2a4d03686",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [12],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1340, 794]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [860, 719],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [11]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [401, 973],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 2\n"]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
|
||||
"pos": [1046, 985],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [12]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [["-1", "string_a"]]
|
||||
},
|
||||
"widgets_values": [""]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 5,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": 5,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"origin_id": 5,
|
||||
"origin_slot": 0,
|
||||
"target_id": 9,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 9,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"origin_id": 9,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 8,
|
||||
"lastLinkId": 10,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [262, 1222, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1330, 1222, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "934a8baa-d79c-428c-8ec9-814ad437d7c7",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [9],
|
||||
"localized_name": "string_a",
|
||||
"pos": [362, 1242]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "4c3d243b-9ff6-4dcd-9dbf-e4ec8e1fc879",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [10],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1350, 1242]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [870, 1038],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 9
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [10]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [442, 1296],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [8]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 3\n"]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 8,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 7,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [-7, 144]
|
||||
},
|
||||
"frontendVersion": "1.38.13"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
760
browser_tests/assets/subgraphs/subgraph-nested-promotion.json
Normal file
@@ -0,0 +1,760 @@
|
||||
{
|
||||
"id": "9a37f747-e96b-4304-9212-7abcaad7bdac",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 18,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PreviewAny",
|
||||
"pos": [1031, 434],
|
||||
"size": [250, 178],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": [null, null, null]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "1e38d8ea-45e1-48a5-aa20-966584201867",
|
||||
"pos": [788, 433.5],
|
||||
"size": [225, 380],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [5]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["3", "string_a"],
|
||||
["4", "value"],
|
||||
["6", "value"],
|
||||
["6", "value_1"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [548, 451],
|
||||
"size": [225, 142],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [4]
|
||||
}
|
||||
],
|
||||
"title": "Outer",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Outer\n"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[4, 1, 0, 5, 0, "STRING"],
|
||||
[5, 5, 0, 2, 0, "STRING"]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "1e38d8ea-45e1-48a5-aa20-966584201867",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 18,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Sub 0",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [351, 432.5, 120, 120]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1352, 294.5, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "7bf3e1d4-0521-4b5c-92f5-47ca598b7eb4",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [1],
|
||||
"localized_name": "string_a",
|
||||
"pos": [451, 452.5]
|
||||
},
|
||||
{
|
||||
"id": "5fb3dcf7-9bfd-4b3c-a1b9-750b4f3edf19",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"linkIds": [13],
|
||||
"pos": [451, 472.5]
|
||||
},
|
||||
{
|
||||
"id": "55d24b8a-7c82-4b02-8e3d-ff31ffb8aa13",
|
||||
"name": "value_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [16],
|
||||
"pos": [451, 492.5]
|
||||
},
|
||||
{
|
||||
"id": "c1fe7cc3-547e-4fb0-b763-61888558d4bd",
|
||||
"name": "value_1_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [18],
|
||||
"pos": [451, 512.5]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "fbe975ba-d7c2-471e-a99a-a1e2c6ab466d",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [9],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1372, 314.5]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [504, 437],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "value",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"title": "Inner 1",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 1\n"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [743, 325],
|
||||
"size": [347, 231],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "9be42452-056b-4c99-9f9f-7381d11c4454",
|
||||
"pos": [1115, 301],
|
||||
"size": [210, 196],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 16
|
||||
},
|
||||
{
|
||||
"name": "value_1",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value_1"
|
||||
},
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [9]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["5", "string_a"],
|
||||
["11", "value"],
|
||||
["9", "value"],
|
||||
["10", "string_a"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 4,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 6,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 3,
|
||||
"target_id": 6,
|
||||
"target_slot": 2,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "9be42452-056b-4c99-9f9f-7381d11c4454",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 18,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Sub 1",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [180, 739, 120, 100]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1246, 612, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "01c05c51-86b5-4bad-b32f-9c911683a13d",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [4],
|
||||
"localized_name": "string_a",
|
||||
"pos": [280, 759]
|
||||
},
|
||||
{
|
||||
"id": "d50f6a62-0185-43d4-a174-a8a94bd8f6e7",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"linkIds": [14],
|
||||
"pos": [280, 779]
|
||||
},
|
||||
{
|
||||
"id": "6b78450e-5986-49cd-b743-c933e5a34a69",
|
||||
"name": "value_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [17],
|
||||
"pos": [280, 799]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "a8bcf3bf-a66a-4c71-8d92-17a2a4d03686",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [12],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1266, 632]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [334, 742],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "value",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 14
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"title": "Inner 2",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 2\n"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [581, 637],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [11]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
|
||||
"pos": [1004, 613],
|
||||
"size": [210, 142],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 11
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [12]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["7", "string_a"],
|
||||
["8", "value"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 11,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 9,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 9,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"origin_id": 9,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 11,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 9,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 18,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Sub 2",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [262, 1222, 120, 80]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1123.089999999999, 1125.1999999999998, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "934a8baa-d79c-428c-8ec9-814ad437d7c7",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [9],
|
||||
"localized_name": "string_a",
|
||||
"pos": [362, 1242]
|
||||
},
|
||||
{
|
||||
"id": "3a545207-7202-42a9-a82f-3b62e1b0f459",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"linkIds": [15],
|
||||
"pos": [362, 1262]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "4c3d243b-9ff6-4dcd-9dbf-e4ec8e1fc879",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [10],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1143.089999999999, 1145.1999999999998]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 8,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [412.96000000000004, 1228.2399999999996],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "value",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [8]
|
||||
}
|
||||
],
|
||||
"title": "Inner 3",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 3\n"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [686.08, 1132.38],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 9
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [10]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 8,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 7,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 8,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [-412, 11]
|
||||
},
|
||||
"frontendVersion": "1.41.7"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"id": "43e9499c-2512-43b5-a5a1-2485eb65da32",
|
||||
"revision": 0,
|
||||
"last_node_id": 8,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [170.55728894250745, 515.6401487466619],
|
||||
"size": [282.8166809082031, 363.8333435058594],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [7, 9]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "21dea088-e1b4-47a4-a01f-3d1bf4504001",
|
||||
"pos": [500.2639113468392, 519.9960755960157],
|
||||
"size": [464.95001220703125, 615.8333129882812],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [10]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["2", "$$canvas-image-preview"],
|
||||
["4", "$$canvas-image-preview"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "a7a0350a-af99-4d26-9391-450b4f726206",
|
||||
"pos": [1000.5293620197185, 499.9253405678786],
|
||||
"size": [225, 359.8333435058594],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image1",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
},
|
||||
{
|
||||
"name": "image2",
|
||||
"type": "IMAGE",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"proxyWidgets": [["6", "$$canvas-image-preview"]]
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[7, 1, 0, 7, 0, "IMAGE"],
|
||||
[9, 1, 0, 8, 0, "IMAGE"],
|
||||
[10, 7, 0, 8, 1, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "21dea088-e1b4-47a4-a01f-3d1bf4504001",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 8,
|
||||
"lastLinkId": 10,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [297.7833638107301, 502.6302057820892, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1052.8175480718996, 502.6302057820892, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "afc8dbc3-12e6-4b3c-9840-9b398d06e6bd",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1, 2],
|
||||
"localized_name": "images",
|
||||
"pos": [397.7833638107301, 522.6302057820892]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "d0a84974-5f4d-4f4b-b23a-2e7288a9689d",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [5],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": [1072.8175480718996, 522.6302057820892]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PreviewImage",
|
||||
"pos": [767.8225773415076, 602.8695134060456],
|
||||
"size": [225, 303.8333435058594],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PreviewImage",
|
||||
"pos": [754.9358989867657, 188.55375831225257],
|
||||
"size": [225, 303.8333435058594],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "ImageInvert",
|
||||
"pos": [477.783932416778, 542.2440719627998],
|
||||
"size": [225, 71.83333587646484],
|
||||
"flags": {
|
||||
"collapsed": false
|
||||
},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [3, 5]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageInvert"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 3,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": 4,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 2,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "a7a0350a-af99-4d26-9391-450b4f726206",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 8,
|
||||
"lastLinkId": 10,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [973.7423316105073, 561.9744246746379, 120, 80]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1905.487372786412, 581.9744246746379, 120, 40]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "20ac4159-6814-4d40-a217-ea260152b689",
|
||||
"name": "image1",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [4],
|
||||
"localized_name": "image1",
|
||||
"pos": [1073.7423316105073, 581.9744246746379]
|
||||
},
|
||||
{
|
||||
"id": "c3759a8c-914e-4450-bc41-ca683ffce96b",
|
||||
"name": "image2",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [8],
|
||||
"localized_name": "image2",
|
||||
"shape": 7,
|
||||
"pos": [1073.7423316105073, 601.9744246746379]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "ImageStitch",
|
||||
"pos": [1153.7423085222254, 396.2033931749105],
|
||||
"size": [270, 225.1666717529297],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image1",
|
||||
"name": "image1",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "image2",
|
||||
"name": "image2",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [6]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageStitch"
|
||||
},
|
||||
"widgets_values": ["right", true, 0, "white"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "PreviewImage",
|
||||
"pos": [1620.4874189629757, 529.9122050216333],
|
||||
"size": [225, 307.8333435058594],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 6,
|
||||
"origin_id": 5,
|
||||
"origin_slot": 0,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 5,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 5,
|
||||
"target_slot": 1,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7269777827561446,
|
||||
"offset": [-35.273237658266034, -55.17394203309256]
|
||||
},
|
||||
"frontendVersion": "1.40.8"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
170
browser_tests/assets/subgraphs/subgraph-with-preview-node.json
Normal file
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"id": "preview-subgraph-test-001",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"pos": [318.6320139868054, 212.9091015141833],
|
||||
"size": [225, 368],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["10", "filename_prefix"],
|
||||
["10", "$$canvas-image-preview"]
|
||||
],
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.13.0",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.6.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "LoadImage",
|
||||
"pos": [-0.5080003681592018, 211.3051121416672],
|
||||
"size": [282.8333435058594, 364],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [2]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
},
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.13.0",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
}
|
||||
],
|
||||
"links": [[2, 11, 0, 5, 0, "IMAGE"]],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 2,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [300, 350, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [900, 350, 120, 40]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "img-slot-001",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1],
|
||||
"pos": [400, 370]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SaveImage",
|
||||
"pos": [500.0046924937855, 300.0146992076527],
|
||||
"size": [315, 340],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.13.0",
|
||||
"Node name for S&R": "SaveImage",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.6.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.1819400303977265,
|
||||
"offset": [81.66005130613983, -19.028558221588725]
|
||||
},
|
||||
"frontendVersion": "1.40.3",
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": [],
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"id": "save-image-and-webm-test",
|
||||
"revision": 0,
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 100],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [1, 2]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "SaveImage",
|
||||
"pos": [450, 100],
|
||||
"size": [210, 270],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "SaveWEBM",
|
||||
"pos": [450, 450],
|
||||
"size": [210, 368],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI", "vp9", 6, 32]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 10, 0, 11, 0, "IMAGE"],
|
||||
[2, 10, 0, 12, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { ComfyNodeSearchBoxV2 } from './components/ComfyNodeSearchBoxV2'
|
||||
import { ContextMenu } from './components/ContextMenu'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import { BottomPanel } from './components/BottomPanel'
|
||||
@@ -166,6 +167,7 @@ export class ComfyPage {
|
||||
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly searchBoxV2: ComfyNodeSearchBoxV2
|
||||
public readonly menu: ComfyMenu
|
||||
public readonly actionbar: ComfyActionbar
|
||||
public readonly templates: ComfyTemplates
|
||||
@@ -210,6 +212,7 @@ export class ComfyPage {
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.searchBoxV2 = new ComfyNodeSearchBoxV2(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
|
||||
29
browser_tests/fixtures/components/ComfyNodeSearchBoxV2.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
|
||||
export class ComfyNodeSearchBoxV2 {
|
||||
readonly dialog: Locator
|
||||
readonly input: Locator
|
||||
readonly results: Locator
|
||||
readonly filterOptions: Locator
|
||||
|
||||
constructor(readonly page: Page) {
|
||||
this.dialog = page.getByRole('search')
|
||||
this.input = this.dialog.locator('input[type="text"]')
|
||||
this.results = this.dialog.getByTestId('result-item')
|
||||
this.filterOptions = this.dialog.getByTestId('filter-option')
|
||||
}
|
||||
|
||||
categoryButton(categoryId: string): Locator {
|
||||
return this.dialog.getByTestId(`category-${categoryId}`)
|
||||
}
|
||||
|
||||
filterBarButton(name: string): Locator {
|
||||
return this.dialog.getByRole('button', { name })
|
||||
}
|
||||
|
||||
async reload(comfyPage: ComfyPage) {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,7 @@ export class SettingDialog extends BaseDialog {
|
||||
* @param value - The value to set
|
||||
*/
|
||||
async setStringSetting(id: string, value: string) {
|
||||
const settingInputDiv = this.page.locator(
|
||||
`div.settings-container div[id="${id}"]`
|
||||
)
|
||||
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
|
||||
await settingInputDiv.locator('input').fill(value)
|
||||
}
|
||||
|
||||
@@ -34,16 +32,31 @@ export class SettingDialog extends BaseDialog {
|
||||
* @param id - The id of the setting
|
||||
*/
|
||||
async toggleBooleanSetting(id: string) {
|
||||
const settingInputDiv = this.page.locator(
|
||||
`div.settings-container div[id="${id}"]`
|
||||
)
|
||||
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
|
||||
await settingInputDiv.locator('input').click()
|
||||
}
|
||||
|
||||
get searchBox() {
|
||||
return this.root.getByPlaceholder(/Search/)
|
||||
}
|
||||
|
||||
get categories() {
|
||||
return this.root.locator('nav').getByRole('button')
|
||||
}
|
||||
|
||||
category(name: string) {
|
||||
return this.root.locator('nav').getByRole('button', { name })
|
||||
}
|
||||
|
||||
get contentArea() {
|
||||
return this.root.getByRole('main')
|
||||
}
|
||||
|
||||
async goToAboutPanel() {
|
||||
await this.page.getByTestId(TestIds.dialogs.settingsTabAbout).click()
|
||||
await this.page
|
||||
.getByTestId(TestIds.dialogs.about)
|
||||
.waitFor({ state: 'visible' })
|
||||
const aboutButton = this.root.locator('nav').getByRole('button', {
|
||||
name: 'About'
|
||||
})
|
||||
await aboutButton.click()
|
||||
await this.page.waitForSelector('.about-container')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ export class CommandHelper {
|
||||
): Promise<void> {
|
||||
await this.page.evaluate(
|
||||
({ commandId, metadata }) => {
|
||||
return window['app'].extensionManager.command.execute(commandId, {
|
||||
const app = window.app
|
||||
if (!app) throw new Error('window.app is not available')
|
||||
|
||||
return app.extensionManager.command.execute(commandId, {
|
||||
metadata
|
||||
})
|
||||
},
|
||||
|
||||
@@ -115,6 +115,16 @@ export class DragDropHelper {
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
const graphCanvasElement = document.querySelector('#graph-canvas')
|
||||
|
||||
// Keep Litegraph's drag-over node tracking in sync when the drop target is a
|
||||
// Vue node DOM overlay outside of the graph canvas element.
|
||||
if (graphCanvasElement && !graphCanvasElement.contains(targetElement)) {
|
||||
graphCanvasElement.dispatchEvent(
|
||||
new DragEvent('dragover', eventOptions)
|
||||
)
|
||||
}
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
|
||||
@@ -33,6 +33,10 @@ export class NodeOperationsHelper {
|
||||
})
|
||||
}
|
||||
|
||||
async getNodeCount(): Promise<number> {
|
||||
return await this.page.evaluate(() => window.app!.graph.nodes.length)
|
||||
}
|
||||
|
||||
async getNodes(): Promise<LGraphNode[]> {
|
||||
return await this.page.evaluate(() => {
|
||||
return window.app!.graph.nodes
|
||||
|
||||
@@ -36,7 +36,7 @@ export class SubgraphHelper {
|
||||
const currentGraph = app.canvas!.graph!
|
||||
|
||||
// Check if we're in a subgraph
|
||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
||||
if (!('inputNode' in currentGraph)) {
|
||||
throw new Error(
|
||||
'Not in a subgraph - this method only works inside subgraphs'
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ export const TestIds = {
|
||||
settingsContainer: 'settings-container',
|
||||
settingsTabAbout: 'settings-tab-about',
|
||||
confirm: 'confirm-dialog',
|
||||
missingNodes: 'missing-nodes-warning',
|
||||
about: 'about-panel',
|
||||
whatsNewSection: 'whats-new-section'
|
||||
},
|
||||
@@ -46,8 +47,12 @@ export const TestIds = {
|
||||
widgets: {
|
||||
decrement: 'decrement',
|
||||
increment: 'increment',
|
||||
domWidgetTextarea: 'dom-widget-textarea',
|
||||
subgraphEnterButton: 'subgraph-enter-button'
|
||||
},
|
||||
breadcrumb: {
|
||||
subgraph: 'subgraph-breadcrumb'
|
||||
},
|
||||
templates: {
|
||||
content: 'template-workflows-content',
|
||||
workflowCard: (id: string) => `template-workflow-${id}`
|
||||
@@ -70,6 +75,7 @@ export type TestIdValue =
|
||||
| (typeof TestIds.propertiesPanel)[keyof typeof TestIds.propertiesPanel]
|
||||
| (typeof TestIds.node)[keyof typeof TestIds.node]
|
||||
| (typeof TestIds.widgets)[keyof typeof TestIds.widgets]
|
||||
| (typeof TestIds.breadcrumb)[keyof typeof TestIds.breadcrumb]
|
||||
| Exclude<
|
||||
(typeof TestIds.templates)[keyof typeof TestIds.templates],
|
||||
(id: string) => string
|
||||
|
||||
@@ -128,7 +128,8 @@ class NodeSlotReference {
|
||||
nodeSize: [node.size[0], node.size[1]],
|
||||
rawConnectionPos: [rawPos[0], rawPos[1]],
|
||||
convertedPos: [convertedPos[0], convertedPos[1]],
|
||||
currentGraphType: window.app!.canvas.graph!.constructor.name
|
||||
currentGraphType:
|
||||
'inputNode' in window.app!.canvas.graph! ? 'Subgraph' : 'LGraph'
|
||||
}
|
||||
)
|
||||
|
||||
@@ -461,18 +462,44 @@ export class NodeReference {
|
||||
// Try multiple positions to avoid DOM widget interference
|
||||
const clickPositions = [
|
||||
{ x: nodePos.x + nodeSize.width / 2, y: nodePos.y + titleHeight + 5 },
|
||||
{ x: nodePos.x + nodeSize.width / 2, y: nodePos.y + nodeSize.height / 2 },
|
||||
{
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y + nodeSize.height / 2
|
||||
},
|
||||
{ x: nodePos.x + 20, y: nodePos.y + titleHeight + 5 }
|
||||
]
|
||||
|
||||
// Click the enter_subgraph title button (top-right of title bar).
|
||||
// This is more reliable than dblclick on the node body because
|
||||
// promoted DOM widgets can overlay the body and intercept events.
|
||||
const subgraphButtonPos = {
|
||||
x: nodePos.x + nodeSize.width - 15,
|
||||
y: nodePos.y - titleHeight / 2
|
||||
}
|
||||
|
||||
const checkIsInSubgraph = async () => {
|
||||
return this.comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph
|
||||
return graph?.constructor?.name === 'Subgraph'
|
||||
return !!graph && 'inputNode' in graph
|
||||
})
|
||||
}
|
||||
|
||||
await expect(async () => {
|
||||
// Try just clicking the enter button first
|
||||
await this.comfyPage.canvas.click({
|
||||
position: { x: 250, y: 250 },
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
await this.comfyPage.canvas.click({
|
||||
position: subgraphButtonPos,
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
if (await checkIsInSubgraph()) return
|
||||
|
||||
for (const position of clickPositions) {
|
||||
// Clear any selection first
|
||||
await this.comfyPage.canvas.click({
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
interface TestWindow extends Window {
|
||||
__ws__?: Record<string, WebSocket>
|
||||
}
|
||||
|
||||
export const webSocketFixture = base.extend<{
|
||||
ws: { trigger(data: unknown, url?: string): Promise<void> }
|
||||
}>({
|
||||
|
||||
64
browser_tests/helpers/promotedWidgets.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
|
||||
export type PromotedWidgetEntry = [string, string]
|
||||
|
||||
export function isPromotedWidgetEntry(
|
||||
entry: unknown
|
||||
): entry is PromotedWidgetEntry {
|
||||
return (
|
||||
Array.isArray(entry) &&
|
||||
entry.length === 2 &&
|
||||
typeof entry[0] === 'string' &&
|
||||
typeof entry[1] === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizePromotedWidgets(
|
||||
value: unknown
|
||||
): PromotedWidgetEntry[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.filter(isPromotedWidgetEntry)
|
||||
}
|
||||
|
||||
export async function getPromotedWidgets(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string
|
||||
): Promise<PromotedWidgetEntry[]> {
|
||||
const raw = await comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.canvas.graph!.getNodeById(id)
|
||||
return node?.properties?.proxyWidgets ?? []
|
||||
}, nodeId)
|
||||
|
||||
return normalizePromotedWidgets(raw)
|
||||
}
|
||||
|
||||
export async function getPromotedWidgetNames(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string
|
||||
): Promise<string[]> {
|
||||
const promotedWidgets = await getPromotedWidgets(comfyPage, nodeId)
|
||||
return promotedWidgets.map(([, widgetName]) => widgetName)
|
||||
}
|
||||
|
||||
export async function getPromotedWidgetCount(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string
|
||||
): Promise<number> {
|
||||
const promotedWidgets = await getPromotedWidgets(comfyPage, nodeId)
|
||||
return promotedWidgets.length
|
||||
}
|
||||
|
||||
export async function getPromotedWidgetCountByName(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string,
|
||||
widgetName: string
|
||||
): Promise<number> {
|
||||
return comfyPage.page.evaluate(
|
||||
([id, name]) => {
|
||||
const node = window.app!.canvas.graph!.getNodeById(id)
|
||||
const widgets = node?.widgets ?? []
|
||||
return widgets.filter((widget) => widget.name === name).length
|
||||
},
|
||||
[nodeId, widgetName] as const
|
||||
)
|
||||
}
|
||||
@@ -226,9 +226,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
await expect(bottomPanel.shortcuts.manageButton).toBeVisible()
|
||||
await bottomPanel.shortcuts.manageButton.click()
|
||||
|
||||
await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.getByRole('option', { name: 'Keybinding' })
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.settingDialog.root).toBeVisible()
|
||||
await expect(comfyPage.settingDialog.category('Keybinding')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,9 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
|
||||
return (window.app!.extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow?.filename
|
||||
})
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
await expect
|
||||
.poll(() => comfyPage.page.title())
|
||||
.toBe(`*${workflowName} - ComfyUI`)
|
||||
})
|
||||
|
||||
// Failing on CI
|
||||
@@ -51,7 +53,7 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
|
||||
})
|
||||
|
||||
test('Can display default title', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.page.title()).toBe('ComfyUI')
|
||||
await expect.poll(() => comfyPage.page.title()).toBe('ComfyUI')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -160,12 +160,12 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
|
||||
})
|
||||
// Click empty space to trigger a change detection.
|
||||
await comfyPage.canvasOps.clickEmptySpace()
|
||||
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1)
|
||||
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(1)
|
||||
})
|
||||
|
||||
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.canvasOps.pan({ x: 10, y: 10 })
|
||||
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
|
||||
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -244,24 +244,19 @@ test.describe(
|
||||
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
const parsed = await (
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const workflow = localStorage.getItem('workflow')
|
||||
if (!workflow) return null
|
||||
try {
|
||||
const data = JSON.parse(workflow)
|
||||
return Array.isArray(data?.nodes) ? data : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
).jsonValue()
|
||||
const parsed = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.graph!
|
||||
if (typeof graph.serialize !== 'function') {
|
||||
throw new Error('app.graph.serialize is not available')
|
||||
}
|
||||
return graph.serialize() as {
|
||||
nodes: Array<{ bgcolor?: string; color?: string }>
|
||||
}
|
||||
})
|
||||
expect(parsed.nodes).toBeDefined()
|
||||
expect(Array.isArray(parsed.nodes)).toBe(true)
|
||||
for (const node of parsed.nodes) {
|
||||
const nodes = parsed.nodes
|
||||
for (const node of nodes) {
|
||||
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
|
||||
if (node.color) expect(node.color).not.toMatch(/hsla/)
|
||||
}
|
||||
|
||||
32
browser_tests/tests/confirmDialogTextWrap.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Confirm dialog text wrapping', { tag: ['@mobile'] }, () => {
|
||||
test('@mobile confirm dialog buttons are visible with long unbreakable text', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const longFilename = 'workflow_checkpoint_' + 'a'.repeat(200) + '.json'
|
||||
|
||||
await comfyPage.page.evaluate((msg) => {
|
||||
window
|
||||
.app!.extensionManager.dialog.confirm({
|
||||
title: 'Confirm',
|
||||
type: 'default',
|
||||
message: msg
|
||||
})
|
||||
.catch(() => {})
|
||||
}, longFilename)
|
||||
|
||||
const dialog = comfyPage.page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const confirmButton = dialog.getByRole('button', { name: 'Confirm' })
|
||||
await expect(confirmButton).toBeVisible()
|
||||
await expect(confirmButton).toBeInViewport()
|
||||
|
||||
const cancelButton = dialog.getByRole('button', { name: 'Cancel' })
|
||||
await expect(cancelButton).toBeVisible()
|
||||
await expect(cancelButton).toBeInViewport()
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import { expect } from '@playwright/test'
|
||||
import type { Keybinding } from '../../src/platform/keybindings/types'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
|
||||
import { TestIds } from '../fixtures/selectors'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
@@ -15,8 +16,9 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
|
||||
|
||||
// Wait for the element with the .comfy-missing-nodes selector to be visible
|
||||
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
|
||||
const missingNodesWarning = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingNodes
|
||||
)
|
||||
await expect(missingNodesWarning).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -25,8 +27,9 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('missing/missing_nodes_in_subgraph')
|
||||
|
||||
// Wait for the element with the .comfy-missing-nodes selector to be visible
|
||||
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
|
||||
const missingNodesWarning = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingNodes
|
||||
)
|
||||
await expect(missingNodesWarning).toBeVisible()
|
||||
|
||||
// Verify the missing node text includes subgraph context
|
||||
@@ -37,14 +40,15 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
|
||||
})
|
||||
|
||||
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
const missingNodesWarning = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingNodes
|
||||
)
|
||||
|
||||
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
|
||||
await comfyPage.page
|
||||
.locator('.p-dialog')
|
||||
.getByRole('button', { name: 'Close' })
|
||||
.click({ force: true })
|
||||
await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' })
|
||||
await expect(missingNodesWarning).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(missingNodesWarning).not.toBeVisible()
|
||||
|
||||
// Wait for any async operations to complete after dialog closes
|
||||
await comfyPage.nextFrame()
|
||||
@@ -55,22 +59,32 @@ test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
||||
|
||||
// Undo and redo the change
|
||||
await comfyPage.keyboard.undo()
|
||||
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
|
||||
await expect(async () => {
|
||||
await expect(missingNodesWarning).not.toBeVisible()
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
await comfyPage.keyboard.redo()
|
||||
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
|
||||
await expect(async () => {
|
||||
await expect(missingNodesWarning).not.toBeVisible()
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test.describe('Execution error', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Should display an error message when an execution error occurs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for the element with the .comfy-execution-error selector to be visible
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(executionError).toBeVisible()
|
||||
// Wait for the error overlay to be visible
|
||||
const errorOverlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(errorOverlay).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -244,9 +258,13 @@ test.describe('Missing models warning', () => {
|
||||
test.describe('Settings', () => {
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsContent = comfyPage.page.locator('.settings-content')
|
||||
await expect(settingsContent).toBeVisible()
|
||||
const isUsableHeight = await settingsContent.evaluate(
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
await expect(contentArea).toBeVisible()
|
||||
const isUsableHeight = await contentArea.evaluate(
|
||||
(el) => el.clientHeight > 30
|
||||
)
|
||||
expect(isUsableHeight).toBeTruthy()
|
||||
@@ -256,7 +274,9 @@ test.describe('Settings', () => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.locator('.settings-container')
|
||||
const settingsLocator = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
@@ -275,10 +295,15 @@ test.describe('Settings', () => {
|
||||
test('Should persist keybinding setting', async ({ comfyPage }) => {
|
||||
// Open the settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container')
|
||||
await comfyPage.page.waitForSelector('[data-testid="settings-dialog"]')
|
||||
|
||||
// Open the keybinding tab
|
||||
await comfyPage.page.getByLabel('Keybinding').click()
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await settingsDialog
|
||||
.locator('nav [role="button"]', { hasText: 'Keybinding' })
|
||||
.click()
|
||||
await comfyPage.page.waitForSelector(
|
||||
'[placeholder="Search Keybindings..."]'
|
||||
)
|
||||
@@ -298,7 +323,10 @@ test.describe('Settings', () => {
|
||||
await input.press('Alt+n')
|
||||
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
'**/api/settings/Comfy.Keybinding.NewBindings'
|
||||
(req) =>
|
||||
req.url().includes('/api/settings') &&
|
||||
!req.url().includes('/api/settings/') &&
|
||||
req.method() === 'POST'
|
||||
)
|
||||
|
||||
// Save keybinding
|
||||
@@ -326,17 +354,23 @@ test.describe('Support', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
const pagePromise = comfyPage.page.context().waitForEvent('page')
|
||||
|
||||
// Prevent loading the external page
|
||||
await comfyPage.page
|
||||
.context()
|
||||
.route('https://support.comfy.org/**', (route) =>
|
||||
route.fulfill({ body: '<html></html>', contentType: 'text/html' })
|
||||
)
|
||||
|
||||
const popupPromise = comfyPage.page.waitForEvent('popup')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
|
||||
const newPage = await pagePromise
|
||||
const popup = await popupPromise
|
||||
|
||||
await newPage.waitForLoadState('networkidle')
|
||||
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
|
||||
|
||||
const url = new URL(newPage.url())
|
||||
const url = new URL(popup.url())
|
||||
expect(url.hostname).toBe('support.comfy.org')
|
||||
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
|
||||
|
||||
await newPage.close()
|
||||
await popup.close()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -376,7 +410,7 @@ test.describe('Signin dialog', () => {
|
||||
test('Paste content to signin dialog should not paste node on canvas', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeNum = (await comfyPage.nodeOps.getNodes()).length
|
||||
const nodeNum = await comfyPage.nodeOps.getNodeCount()
|
||||
await comfyPage.canvas.click({
|
||||
position: DefaultGraphPositions.emptyLatentWidgetClick
|
||||
})
|
||||
@@ -399,6 +433,6 @@ test.describe('Signin dialog', () => {
|
||||
await input.press('Control+v')
|
||||
await expect(input).toHaveValue('test_password')
|
||||
|
||||
expect(await comfyPage.nodeOps.getNodes()).toHaveLength(nodeNum)
|
||||
expect(await comfyPage.nodeOps.getNodeCount()).toBe(nodeNum)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,22 +7,29 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
})
|
||||
|
||||
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test(
|
||||
'Report error on unconnected slot',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
await comfyPage.canvasOps.clickEmptySpace()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
).toBeVisible()
|
||||
await comfyPage.page
|
||||
.locator('.p-dialog')
|
||||
.getByRole('button', { name: 'Close' })
|
||||
.locator('[data-testid="error-overlay"]')
|
||||
.getByRole('button', { name: 'Dismiss' })
|
||||
.click()
|
||||
await comfyPage.page.locator('.comfy-error-report').waitFor({
|
||||
state: 'hidden'
|
||||
})
|
||||
await comfyPage.page
|
||||
.locator('[data-testid="error-overlay"]')
|
||||
.waitFor({ state: 'hidden' })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'execution-error-unconnected-slot.png'
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
@@ -10,6 +10,8 @@ import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
})
|
||||
|
||||
test.describe('Group Node', { tag: '@node' }, () => {
|
||||
|
||||
@@ -13,6 +13,9 @@ import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
// Wait for the legacy menu to appear and canvas to settle after layout shift.
|
||||
await comfyPage.page.locator('.comfy-menu').waitFor({ state: 'visible' })
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
|
||||
@@ -168,6 +171,7 @@ test.describe('Node Interaction', () => {
|
||||
|
||||
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.nodeOps.dragTextEncodeNode2()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
|
||||
})
|
||||
|
||||
@@ -733,6 +737,25 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
// Wait for V2 persistence debounce to save the modified workflow
|
||||
const start = Date.now()
|
||||
await comfyPage.page.waitForFunction((since) => {
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i)
|
||||
if (!key?.startsWith('Comfy.Workflow.DraftIndex.v2:')) continue
|
||||
const json = window.localStorage.getItem(key)
|
||||
if (!json) continue
|
||||
try {
|
||||
const index = JSON.parse(json)
|
||||
if (typeof index.updatedAt === 'number' && index.updatedAt >= since) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, start)
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
@@ -755,10 +778,17 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
||||
|
||||
// Wait for localStorage to persist the workflow paths before reloading
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
|
||||
)
|
||||
// Wait for sessionStorage to persist the workflow paths before reloading
|
||||
// V2 persistence uses sessionStorage with client-scoped keys
|
||||
await comfyPage.page.waitForFunction(() => {
|
||||
for (let i = 0; i < window.sessionStorage.length; i++) {
|
||||
const key = window.sessionStorage.key(i)
|
||||
if (key?.startsWith('Comfy.Workflow.OpenPaths:')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
@@ -203,19 +203,19 @@ test.describe('Menu', { tag: '@ui' }, () => {
|
||||
await topbar.switchTheme('light')
|
||||
|
||||
// Verify menu stays open and Light theme shows as active
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeSubmenu).toBeVisible()
|
||||
|
||||
// Check that Light theme is active
|
||||
expect(await topbar.isMenuItemActive(lightThemeItem)).toBe(true)
|
||||
await expect(async () => {
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeSubmenu).toBeVisible()
|
||||
expect(await topbar.isMenuItemActive(lightThemeItem)).toBe(true)
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
// Screenshot with light theme active
|
||||
await comfyPage.attachScreenshot('theme-menu-light-active')
|
||||
|
||||
// Verify ColorPalette setting is set to "light"
|
||||
expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe(
|
||||
'light'
|
||||
)
|
||||
await expect
|
||||
.poll(() => comfyPage.settings.getSetting('Comfy.ColorPalette'))
|
||||
.toBe('light')
|
||||
|
||||
// Close menu to see theme change
|
||||
await topbar.closeTopbarMenu()
|
||||
@@ -228,20 +228,22 @@ test.describe('Menu', { tag: '@ui' }, () => {
|
||||
await topbar.switchTheme('dark')
|
||||
|
||||
// Verify menu stays open and Dark theme shows as active
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeItems2.submenu).toBeVisible()
|
||||
|
||||
// Check that Dark theme is active and Light theme is not
|
||||
expect(await topbar.isMenuItemActive(themeItems2.darkTheme)).toBe(true)
|
||||
expect(await topbar.isMenuItemActive(themeItems2.lightTheme)).toBe(false)
|
||||
await expect(async () => {
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeItems2.submenu).toBeVisible()
|
||||
expect(await topbar.isMenuItemActive(themeItems2.darkTheme)).toBe(true)
|
||||
expect(await topbar.isMenuItemActive(themeItems2.lightTheme)).toBe(
|
||||
false
|
||||
)
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
// Screenshot with dark theme active
|
||||
await comfyPage.attachScreenshot('theme-menu-dark-active')
|
||||
|
||||
// Verify ColorPalette setting is set to "dark"
|
||||
expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe(
|
||||
'dark'
|
||||
)
|
||||
await expect
|
||||
.poll(() => comfyPage.settings.getSetting('Comfy.ColorPalette'))
|
||||
.toBe('dark')
|
||||
|
||||
// Close menu
|
||||
await topbar.closeTopbarMenu()
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 29 KiB |
@@ -27,6 +27,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
|
||||
})
|
||||
|
||||
test.describe('Selection Toolbox', () => {
|
||||
|
||||
@@ -18,7 +18,10 @@ test.describe('Node search box', { tag: '@node' }, () => {
|
||||
'Comfy.LinkRelease.ActionShift',
|
||||
'search box'
|
||||
)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
})
|
||||
|
||||
test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => {
|
||||
@@ -45,7 +48,10 @@ test.describe('Node search box', { tag: '@node' }, () => {
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
// Simulate new user with 1.24.1+ installed version
|
||||
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
// Don't set LinkRelease settings explicitly to test versioned defaults
|
||||
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
@@ -215,6 +221,14 @@ test.describe('Node search box', { tag: '@node' }, () => {
|
||||
await expectFilterChips(comfyPage, ['MODEL', 'CLIP'])
|
||||
})
|
||||
|
||||
test('Does not add duplicate filter with same type and value', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
|
||||
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
|
||||
await expectFilterChips(comfyPage, ['MODEL'])
|
||||
})
|
||||
|
||||
test('Can remove filter', async ({ comfyPage }) => {
|
||||
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
|
||||
await comfyPage.searchBox.removeFilter(0)
|
||||
@@ -277,7 +291,10 @@ test.describe('Release context menu', { tag: '@node' }, () => {
|
||||
'Comfy.LinkRelease.ActionShift',
|
||||
'search box'
|
||||
)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
@@ -321,7 +338,10 @@ test.describe('Release context menu', { tag: '@node' }, () => {
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
// Simulate existing user with pre-1.24.1 version
|
||||
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.23.0')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
// Don't set LinkRelease settings explicitly to test versioned defaults
|
||||
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
@@ -342,7 +362,10 @@ test.describe('Release context menu', { tag: '@node' }, () => {
|
||||
'Comfy.LinkRelease.Action',
|
||||
'context menu'
|
||||
)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
// Context menu should appear due to explicit setting, not search box
|
||||
|
||||
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 90 KiB |
149
browser_tests/tests/nodeSearchBoxV2.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Node search box V2', { tag: '@node' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.LinkRelease.Action',
|
||||
'search box'
|
||||
)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.LinkRelease.ActionShift',
|
||||
'search box'
|
||||
)
|
||||
await comfyPage.searchBoxV2.reload(comfyPage)
|
||||
})
|
||||
|
||||
test('Can open search and add node', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
|
||||
await searchBoxV2.input.fill('KSampler')
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(newCount).toBe(initialCount + 1)
|
||||
})
|
||||
|
||||
test('Can add first default result with Enter', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
|
||||
// Default results should be visible without typing
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
|
||||
// Enter should add the first (selected) result
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(newCount).toBe(initialCount + 1)
|
||||
})
|
||||
|
||||
test.describe('Category navigation', () => {
|
||||
test('Favorites shows only bookmarked nodes', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
|
||||
'KSampler'
|
||||
])
|
||||
await searchBoxV2.reload(comfyPage)
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
|
||||
await searchBoxV2.categoryButton('favorites').click()
|
||||
|
||||
await expect(searchBoxV2.results).toHaveCount(1)
|
||||
await expect(searchBoxV2.results.first()).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('Category filters results to matching nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
|
||||
await searchBoxV2.categoryButton('sampling').click()
|
||||
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
const count = await searchBoxV2.results.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Filter workflow', () => {
|
||||
test('Can filter by input type via filter bar', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
|
||||
// Click "Input" filter chip in the filter bar
|
||||
await searchBoxV2.filterBarButton('Input').click()
|
||||
|
||||
// Filter options should appear
|
||||
await expect(searchBoxV2.filterOptions.first()).toBeVisible()
|
||||
|
||||
// Type to narrow and select MODEL
|
||||
await searchBoxV2.input.fill('MODEL')
|
||||
await searchBoxV2.filterOptions
|
||||
.filter({ hasText: 'MODEL' })
|
||||
.first()
|
||||
.click()
|
||||
|
||||
// Filter chip should appear and results should be filtered
|
||||
await expect(
|
||||
searchBoxV2.dialog.getByText('Input:', { exact: false }).locator('..')
|
||||
).toContainText('MODEL')
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Keyboard navigation', () => {
|
||||
test('Can navigate and select with keyboard', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
|
||||
await searchBoxV2.input.fill('KSampler')
|
||||
const results = searchBoxV2.results
|
||||
await expect(results.first()).toBeVisible()
|
||||
|
||||
// First result selected by default
|
||||
await expect(results.first()).toHaveAttribute('aria-selected', 'true')
|
||||
|
||||
// ArrowDown moves selection
|
||||
await comfyPage.page.keyboard.press('ArrowDown')
|
||||
await expect(results.nth(1)).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(results.first()).toHaveAttribute('aria-selected', 'false')
|
||||
|
||||
// ArrowUp moves back
|
||||
await comfyPage.page.keyboard.press('ArrowUp')
|
||||
await expect(results.first()).toHaveAttribute('aria-selected', 'true')
|
||||
|
||||
// Enter selects and adds node
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(newCount).toBe(initialCount + 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { TestIds } from '../../fixtures/selectors'
|
||||
|
||||
test.describe('Properties panel position', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
|
||||
// Open a sidebar tab to ensure sidebar is visible
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
})
|
||||
|
||||
test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
|
||||
|
||||
@@ -53,6 +53,11 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Loading options', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 89 KiB |
42
browser_tests/tests/saveImageAndWebp.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe(
|
||||
'Save Image and WEBM preview',
|
||||
{ tag: ['@screenshot', '@widget'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('Can preview both SaveImage and SaveWEBM outputs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'widgets/save_image_and_animated_webp'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await comfyPage.runButton.click()
|
||||
|
||||
const saveImageNode = comfyPage.vueNodes.getNodeByTitle('Save Image')
|
||||
const saveWebmNode = comfyPage.vueNodes.getNodeByTitle('SaveWEBM')
|
||||
|
||||
// Wait for SaveImage to render an img inside .image-preview
|
||||
await expect(saveImageNode.locator('.image-preview img')).toBeVisible({
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
// Wait for SaveWEBM to render a video inside .video-preview
|
||||
await expect(saveWebmNode.locator('.video-preview video')).toBeVisible({
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
await expect(comfyPage.page).toHaveScreenshot(
|
||||
'save-image-and-webm-preview.png'
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -5,6 +5,7 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
test.describe('Node library sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeLibrary.BookmarksCustomization',
|
||||
|
||||
@@ -83,17 +83,15 @@ test.describe('Workflows sidebar', () => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
|
||||
const originalNodeCount = (await comfyPage.nodeOps.getNodes()).length
|
||||
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
|
||||
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length))
|
||||
.poll(() => comfyPage.nodeOps.getNodeCount())
|
||||
.toEqual(originalNodeCount + 1)
|
||||
|
||||
await tab.getPersistedItem('workflow1.json').click()
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length))
|
||||
.toEqual(1)
|
||||
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
|
||||
})
|
||||
|
||||
test('Can rename nested workflow from opened workflow item', async ({
|
||||
@@ -123,17 +121,14 @@ test.describe('Workflows sidebar', () => {
|
||||
test('Can save workflow as', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'workflow3.json'
|
||||
])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'workflow3.json',
|
||||
'workflow4.json'
|
||||
])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Exported workflow does not contain localized slot names', async ({
|
||||
@@ -220,24 +215,22 @@ test.describe('Workflows sidebar', () => {
|
||||
await topbar.saveWorkflow('workflow1.json')
|
||||
await topbar.saveWorkflowAs('workflow2.json')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'workflow2.json'
|
||||
])
|
||||
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
|
||||
'workflow2.json'
|
||||
)
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['workflow1.json', 'workflow2.json'])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||
.toEqual('workflow2.json')
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow2.json',
|
||||
'workflow1.json'
|
||||
])
|
||||
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
|
||||
'workflow1.json'
|
||||
)
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['workflow2.json', 'workflow1.json'])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||
.toEqual('workflow1.json')
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
|
||||
100
browser_tests/tests/subgraph-duplicate-ids.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Subgraph duplicate ID remapping', { tag: ['@subgraph'] }, () => {
|
||||
const WORKFLOW = 'subgraphs/subgraph-nested-duplicate-ids'
|
||||
|
||||
test('All node IDs are globally unique after loading', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
|
||||
const result = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
// TODO: Extract allGraphs accessor (root + subgraphs) into LGraph
|
||||
// TODO: Extract allNodeIds accessor into LGraph
|
||||
const allGraphs = [graph, ...graph.subgraphs.values()]
|
||||
const allIds = allGraphs
|
||||
.flatMap((g) => g._nodes)
|
||||
.map((n) => n.id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
|
||||
return { allIds, uniqueCount: new Set(allIds).size }
|
||||
})
|
||||
|
||||
expect(result.uniqueCount).toBe(result.allIds.length)
|
||||
expect(result.allIds.length).toBeGreaterThanOrEqual(10)
|
||||
})
|
||||
|
||||
test('Root graph node IDs are preserved as canonical', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
|
||||
const rootIds = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
return graph._nodes
|
||||
.map((n) => n.id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
.sort((a, b) => a - b)
|
||||
})
|
||||
|
||||
expect(rootIds).toEqual([1, 2, 5])
|
||||
})
|
||||
|
||||
test('All links reference valid nodes in their graph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
|
||||
const invalidLinks = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
const labeledGraphs: [string, typeof graph][] = [
|
||||
['root', graph],
|
||||
...[...graph.subgraphs.entries()].map(
|
||||
([id, sg]) => [`subgraph:${id}`, sg] as [string, typeof graph]
|
||||
)
|
||||
]
|
||||
|
||||
const isNonNegative = (id: number | string) =>
|
||||
typeof id === 'number' && id >= 0
|
||||
|
||||
return labeledGraphs.flatMap(([label, g]) =>
|
||||
[...g._links.values()].flatMap((link) =>
|
||||
[
|
||||
isNonNegative(link.origin_id) &&
|
||||
!g._nodes_by_id[link.origin_id] &&
|
||||
`${label}: origin_id ${link.origin_id} not found`,
|
||||
isNonNegative(link.target_id) &&
|
||||
!g._nodes_by_id[link.target_id] &&
|
||||
`${label}: target_id ${link.target_id} not found`
|
||||
].filter(Boolean)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
expect(invalidLinks).toEqual([])
|
||||
})
|
||||
|
||||
test('Subgraph navigation works after ID remapping', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('5')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const isInSubgraph = () =>
|
||||
comfyPage.page.evaluate(
|
||||
() => window.app!.canvas.graph?.isRootGraph === false
|
||||
)
|
||||
|
||||
expect(await isInSubgraph()).toBe(true)
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await isInSubgraph()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -19,6 +19,10 @@ const SELECTORS = {
|
||||
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
})
|
||||
|
||||
// Helper to get subgraph slot count
|
||||
@@ -49,7 +53,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
): Promise<boolean> {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph
|
||||
return graph?.constructor?.name === 'Subgraph'
|
||||
return !!graph && 'inputNode' in graph
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +65,10 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
|
||||
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
const [vaeEncodeNode] = await comfyPage.nodeOps.getNodeRefsByType(
|
||||
'VAEEncode',
|
||||
true
|
||||
)
|
||||
|
||||
await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0)
|
||||
await comfyPage.nextFrame()
|
||||
@@ -77,7 +84,10 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
|
||||
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
const [vaeEncodeNode] = await comfyPage.nodeOps.getNodeRefsByType(
|
||||
'VAEEncode',
|
||||
true
|
||||
)
|
||||
|
||||
await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0)
|
||||
await comfyPage.nextFrame()
|
||||
@@ -820,7 +830,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
// Open settings dialog using hotkey
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container', {
|
||||
await comfyPage.page.waitForSelector('[data-testid="settings-dialog"]', {
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
@@ -830,7 +840,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
// Dialog should be closed
|
||||
await expect(
|
||||
comfyPage.page.locator('.settings-container')
|
||||
comfyPage.page.locator('[data-testid="settings-dialog"]')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Should still be in subgraph
|
||||
|
||||
690
browser_tests/tests/subgraphPromotion.spec.ts
Normal file
@@ -0,0 +1,690 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { TestIds } from '../fixtures/selectors'
|
||||
import { fitToViewInstant } from '../helpers/fitToView'
|
||||
import {
|
||||
getPromotedWidgetNames,
|
||||
getPromotedWidgetCount,
|
||||
getPromotedWidgets
|
||||
} from '../helpers/promotedWidgets'
|
||||
|
||||
/**
|
||||
* Check whether we're currently in a subgraph.
|
||||
*/
|
||||
async function isInSubgraph(comfyPage: ComfyPage): Promise<boolean> {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph
|
||||
return !!graph && 'inputNode' in graph
|
||||
})
|
||||
}
|
||||
|
||||
async function exitSubgraphViaBreadcrumb(comfyPage: ComfyPage): Promise<void> {
|
||||
const breadcrumb = comfyPage.page.getByTestId(TestIds.breadcrumb.subgraph)
|
||||
await breadcrumb.waitFor({ state: 'visible', timeout: 5000 })
|
||||
|
||||
const parentLink = breadcrumb.getByRole('link').first()
|
||||
await expect(parentLink).toBeVisible()
|
||||
await parentLink.click()
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
test.describe(
|
||||
'Subgraph Widget Promotion',
|
||||
{ tag: ['@subgraph', '@widget'] },
|
||||
() => {
|
||||
test.describe('Auto-promotion on Convert to Subgraph', () => {
|
||||
test('Recommended widgets are auto-promoted when creating a subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
|
||||
// Select just the KSampler node (id 3) which has a "seed" widget
|
||||
const ksampler = await comfyPage.nodeOps.getNodeRefById('3')
|
||||
await ksampler.click('title')
|
||||
const subgraphNode = await ksampler.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// SubgraphNode should exist
|
||||
expect(await subgraphNode.exists()).toBe(true)
|
||||
|
||||
// The KSampler has a "seed" widget which is in the recommended list.
|
||||
// The promotion store should have at least the seed widget promoted.
|
||||
const nodeId = String(subgraphNode.id)
|
||||
const promotedNames = await getPromotedWidgetNames(comfyPage, nodeId)
|
||||
expect(promotedNames).toContain('seed')
|
||||
|
||||
// SubgraphNode should have widgets (promoted views)
|
||||
const widgetCount = await getPromotedWidgetCount(comfyPage, nodeId)
|
||||
expect(widgetCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('CLIPTextEncode text widget is auto-promoted', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
|
||||
// Select the positive CLIPTextEncode node (id 6)
|
||||
const clipNode = await comfyPage.nodeOps.getNodeRefById('6')
|
||||
await clipNode.click('title')
|
||||
const subgraphNode = await clipNode.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const nodeId = String(subgraphNode.id)
|
||||
const promotedNames = await getPromotedWidgetNames(comfyPage, nodeId)
|
||||
expect(promotedNames.length).toBeGreaterThan(0)
|
||||
|
||||
// CLIPTextEncode is in the recommendedNodes list, so its text widget
|
||||
// should be promoted
|
||||
expect(promotedNames).toContain('text')
|
||||
})
|
||||
|
||||
test('SaveImage/PreviewImage nodes get pseudo-widget promoted', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await fitToViewInstant(comfyPage)
|
||||
|
||||
// Select the SaveImage node (id 9 in default workflow)
|
||||
const saveNode = await comfyPage.nodeOps.getNodeRefById('9')
|
||||
await saveNode.click('title')
|
||||
const subgraphNode = await saveNode.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const promotedNames = await getPromotedWidgetNames(
|
||||
comfyPage,
|
||||
String(subgraphNode.id)
|
||||
)
|
||||
|
||||
// SaveImage is in the recommendedNodes list, so filename_prefix is promoted
|
||||
expect(promotedNames).toContain('filename_prefix')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Promoted Widget Visibility in LiteGraph Mode', () => {
|
||||
test('Promoted text widget is visible on SubgraphNode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// The subgraph node (id 11) should have a text widget promoted
|
||||
const textarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(textarea).toBeVisible()
|
||||
await expect(textarea).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('Multiple promoted widgets all render on SubgraphNode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-multiple-promoted-widgets'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const textareas = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(textareas.first()).toBeVisible()
|
||||
const count = await textareas.count()
|
||||
expect(count).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Promoted Widget Visibility in Vue Mode', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
})
|
||||
|
||||
test('Promoted text widget renders on SubgraphNode in Vue mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// SubgraphNode (id 11) should render with its body
|
||||
const subgraphVueNode = comfyPage.vueNodes.getNodeLocator('11')
|
||||
await expect(subgraphVueNode).toBeVisible()
|
||||
|
||||
// It should have the Enter Subgraph button
|
||||
const enterButton = subgraphVueNode.getByTestId('subgraph-enter-button')
|
||||
await expect(enterButton).toBeVisible()
|
||||
|
||||
// The promoted text widget should render inside the node
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-11"]')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
|
||||
// Widgets section should exist and have at least one widget
|
||||
const widgets = nodeBody.locator('.lg-node-widgets > div')
|
||||
await expect(widgets.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Enter Subgraph button navigates into subgraph in Vue mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await comfyPage.vueNodes.enterSubgraph('11')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
})
|
||||
|
||||
test('Multiple promoted widgets render on SubgraphNode in Vue mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-multiple-promoted-widgets'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const subgraphVueNode = comfyPage.vueNodes.getNodeLocator('11')
|
||||
await expect(subgraphVueNode).toBeVisible()
|
||||
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-11"]')
|
||||
const widgets = nodeBody.locator('.lg-node-widgets > div')
|
||||
const count = await widgets.count()
|
||||
expect(count).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Promoted Widget Reactivity', () => {
|
||||
test('Value changes on promoted widget sync to interior widget', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const testContent = 'promoted-value-sync-test'
|
||||
|
||||
// Type into the promoted textarea on the SubgraphNode
|
||||
const textarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await textarea.fill(testContent)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate into subgraph
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
// Interior CLIPTextEncode textarea should have the same value
|
||||
const interiorTextarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(interiorTextarea).toHaveValue(testContent)
|
||||
})
|
||||
|
||||
test('Value changes on interior widget sync to promoted widget', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const testContent = 'interior-value-sync-test'
|
||||
|
||||
// Navigate into subgraph
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
// Type into the interior CLIPTextEncode textarea
|
||||
const interiorTextarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await interiorTextarea.fill(testContent)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back to parent graph
|
||||
await exitSubgraphViaBreadcrumb(comfyPage)
|
||||
|
||||
// Promoted textarea on SubgraphNode should have the same value
|
||||
const promotedTextarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(promotedTextarea).toHaveValue(testContent)
|
||||
})
|
||||
|
||||
test('Value persists through repeated navigation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const testContent = 'persistence-through-navigation'
|
||||
|
||||
// Set value on promoted widget
|
||||
const textarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await textarea.fill(testContent)
|
||||
|
||||
// Navigate in and out multiple times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
const interiorTextarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(interiorTextarea).toHaveValue(testContent)
|
||||
|
||||
await exitSubgraphViaBreadcrumb(comfyPage)
|
||||
|
||||
const promotedTextarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(promotedTextarea).toHaveValue(testContent)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Manual Promote/Demote via Context Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Can promote a widget from inside a subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
// Get the KSampler node (id 1) inside the subgraph
|
||||
const ksampler = await comfyPage.nodeOps.getNodeRefById('1')
|
||||
|
||||
// Right-click on the KSampler's "steps" widget (index 2) to promote it
|
||||
const stepsWidget = await ksampler.getWidget(2)
|
||||
const widgetPos = await stepsWidget.getPosition()
|
||||
await comfyPage.canvas.click({
|
||||
position: widgetPos,
|
||||
button: 'right',
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Look for the Promote Widget menu entry
|
||||
const promoteEntry = comfyPage.page
|
||||
.locator('.litemenu-entry')
|
||||
.filter({ hasText: /Promote Widget/ })
|
||||
|
||||
await expect(promoteEntry).toBeVisible()
|
||||
await promoteEntry.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back to parent
|
||||
await exitSubgraphViaBreadcrumb(comfyPage)
|
||||
|
||||
// SubgraphNode should now have the promoted widget
|
||||
const widgetCount = await getPromotedWidgetCount(comfyPage, '2')
|
||||
expect(widgetCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Can un-promote a widget from inside a subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
// First promote a canvas-rendered widget (KSampler "steps")
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const ksampler = await comfyPage.nodeOps.getNodeRefById('1')
|
||||
const stepsWidget = await ksampler.getWidget(2)
|
||||
const widgetPos = await stepsWidget.getPosition()
|
||||
|
||||
await comfyPage.canvas.click({
|
||||
position: widgetPos,
|
||||
button: 'right',
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const promoteEntry = comfyPage.page
|
||||
.locator('.litemenu-entry')
|
||||
.filter({ hasText: /Promote Widget/ })
|
||||
await expect(promoteEntry).toBeVisible()
|
||||
await promoteEntry.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back and verify promotion took effect
|
||||
await exitSubgraphViaBreadcrumb(comfyPage)
|
||||
await fitToViewInstant(comfyPage)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const initialWidgetCount = await getPromotedWidgetCount(comfyPage, '2')
|
||||
expect(initialWidgetCount).toBeGreaterThan(0)
|
||||
|
||||
// Navigate back in and un-promote
|
||||
const subgraphNode2 = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
await subgraphNode2.navigateIntoSubgraph()
|
||||
const stepsWidget2 = await (
|
||||
await comfyPage.nodeOps.getNodeRefById('1')
|
||||
).getWidget(2)
|
||||
const widgetPos2 = await stepsWidget2.getPosition()
|
||||
|
||||
await comfyPage.canvas.click({
|
||||
position: widgetPos2,
|
||||
button: 'right',
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const unpromoteEntry = comfyPage.page
|
||||
.locator('.litemenu-entry')
|
||||
.filter({ hasText: /Un-Promote Widget/ })
|
||||
|
||||
await expect(unpromoteEntry).toBeVisible()
|
||||
await unpromoteEntry.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back to parent
|
||||
await exitSubgraphViaBreadcrumb(comfyPage)
|
||||
|
||||
// SubgraphNode should have fewer widgets
|
||||
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '2')
|
||||
expect(finalWidgetCount).toBeLessThan(initialWidgetCount)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Pseudo-Widget Promotion', () => {
|
||||
test('Promotion store tracks pseudo-widget entries for subgraph with preview node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-preview-node'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// The SaveImage node is in the recommendedNodes list, so its
|
||||
// filename_prefix widget should be auto-promoted
|
||||
const promotedNames = await getPromotedWidgetNames(comfyPage, '5')
|
||||
expect(promotedNames.length).toBeGreaterThan(0)
|
||||
expect(promotedNames).toContain('filename_prefix')
|
||||
})
|
||||
|
||||
test('Converting SaveImage to subgraph promotes its widgets', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await fitToViewInstant(comfyPage)
|
||||
|
||||
// Select SaveImage (id 9)
|
||||
const saveNode = await comfyPage.nodeOps.getNodeRefById('9')
|
||||
await saveNode.click('title')
|
||||
const subgraphNode = await saveNode.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// SaveImage is a recommended node, so filename_prefix should be promoted
|
||||
const nodeId = String(subgraphNode.id)
|
||||
const promotedNames = await getPromotedWidgetNames(comfyPage, nodeId)
|
||||
expect(promotedNames.length).toBeGreaterThan(0)
|
||||
|
||||
const widgetCount = await getPromotedWidgetCount(comfyPage, nodeId)
|
||||
expect(widgetCount).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Legacy And Round-Trip Coverage', () => {
|
||||
test('Legacy -1 proxyWidgets entries are hydrated to concrete interior node IDs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-compressed-target-slot'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const promotedWidgets = await getPromotedWidgets(comfyPage, '2')
|
||||
expect(promotedWidgets.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
promotedWidgets.some(([interiorNodeId]) => interiorNodeId === '-1')
|
||||
).toBe(false)
|
||||
expect(
|
||||
promotedWidgets.some(
|
||||
([interiorNodeId, widgetName]) =>
|
||||
interiorNodeId !== '-1' && widgetName === 'batch_size'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('Promoted widgets survive serialize -> loadGraphData round-trip', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const beforePromoted = await getPromotedWidgetNames(comfyPage, '11')
|
||||
expect(beforePromoted).toContain('text')
|
||||
|
||||
const serialized = await comfyPage.page.evaluate(() => {
|
||||
return window.app!.graph!.serialize()
|
||||
})
|
||||
|
||||
await comfyPage.page.evaluate((workflow: ComfyWorkflowJSON) => {
|
||||
return window.app!.loadGraphData(workflow)
|
||||
}, serialized as ComfyWorkflowJSON)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const afterPromoted = await getPromotedWidgetNames(comfyPage, '11')
|
||||
expect(afterPromoted).toContain('text')
|
||||
|
||||
const widgetCount = await getPromotedWidgetCount(comfyPage, '11')
|
||||
expect(widgetCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Cloning a subgraph node keeps promoted widget entries on original and clone', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const originalNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
const originalPos = await originalNode.getPosition()
|
||||
|
||||
await comfyPage.page.mouse.move(originalPos.x + 16, originalPos.y + 16)
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.mouse.move(originalPos.x + 72, originalPos.y + 72)
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNodeIds = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
return graph.nodes
|
||||
.filter(
|
||||
(n) =>
|
||||
typeof n.isSubgraphNode === 'function' && n.isSubgraphNode()
|
||||
)
|
||||
.map((n) => String(n.id))
|
||||
})
|
||||
|
||||
expect(subgraphNodeIds.length).toBeGreaterThan(1)
|
||||
for (const nodeId of subgraphNodeIds) {
|
||||
const promotedWidgets = await getPromotedWidgets(comfyPage, nodeId)
|
||||
expect(promotedWidgets.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
promotedWidgets.some(([, widgetName]) => widgetName === 'text')
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Vue Mode - Promoted Preview Content', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
})
|
||||
|
||||
test('SubgraphNode with preview node shows hasCustomContent area in Vue mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-preview-node'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const subgraphVueNode = comfyPage.vueNodes.getNodeLocator('5')
|
||||
await expect(subgraphVueNode).toBeVisible()
|
||||
|
||||
// The node body should exist
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-5"]')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Nested Promoted Widget Disabled State', () => {
|
||||
test('Externally linked promoted widget is disabled, unlinked ones are not', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-nested-promotion'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Node 5 (Sub 0) has 4 promoted widgets. The first (string_a) has its
|
||||
// slot connected externally from the Outer node, so it should be
|
||||
// disabled. The remaining promoted textarea widgets (value, value_1)
|
||||
// are unlinked and should be enabled.
|
||||
const promotedNames = await getPromotedWidgetNames(comfyPage, '5')
|
||||
expect(promotedNames).toContain('string_a')
|
||||
expect(promotedNames).toContain('value')
|
||||
|
||||
const disabledState = await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.canvas.graph!.getNodeById('5')
|
||||
return (node?.widgets ?? []).map((w) => ({
|
||||
name: w.name,
|
||||
disabled: !!w.computedDisabled
|
||||
}))
|
||||
})
|
||||
|
||||
const linkedWidget = disabledState.find((w) => w.name === 'string_a')
|
||||
expect(linkedWidget?.disabled).toBe(true)
|
||||
|
||||
const unlinkedWidgets = disabledState.filter(
|
||||
(w) => w.name !== 'string_a'
|
||||
)
|
||||
for (const w of unlinkedWidgets) {
|
||||
expect(w.disabled).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
test('Unlinked promoted textarea widgets are editable on the subgraph exterior', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-nested-promotion'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// The promoted textareas that are NOT externally linked should be
|
||||
// fully opaque and interactive.
|
||||
const textareas = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(textareas.first()).toBeVisible()
|
||||
|
||||
const count = await textareas.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const textarea = textareas.nth(i)
|
||||
const wrapper = textarea.locator('..')
|
||||
const opacity = await wrapper.evaluate(
|
||||
(el) => getComputedStyle(el).opacity
|
||||
)
|
||||
|
||||
if (opacity === '1' && (await textarea.isEditable())) {
|
||||
const testContent = `nested-promotion-edit-${i}`
|
||||
await textarea.fill(testContent)
|
||||
await expect(textarea).toHaveValue(testContent)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Promotion Cleanup', () => {
|
||||
test('Removing subgraph node clears promotion store entries', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify promotions exist
|
||||
const namesBefore = await getPromotedWidgetNames(comfyPage, '11')
|
||||
expect(namesBefore.length).toBeGreaterThan(0)
|
||||
|
||||
// Delete the subgraph node
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
await subgraphNode.click('title')
|
||||
await comfyPage.page.keyboard.press('Delete')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Node no longer exists, so promoted widgets should be gone
|
||||
const nodeExists = await comfyPage.page.evaluate(() => {
|
||||
return !!window.app!.canvas.graph!.getNodeById('11')
|
||||
})
|
||||
expect(nodeExists).toBe(false)
|
||||
})
|
||||
|
||||
test('Removing I/O slot removes associated promoted widget', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
const initialWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
|
||||
expect(initialWidgetCount).toBeGreaterThan(0)
|
||||
|
||||
// Navigate into subgraph
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
// Remove the text input slot
|
||||
await comfyPage.subgraph.rightClickInputSlot('text')
|
||||
await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back via breadcrumb
|
||||
await comfyPage.page
|
||||
.getByTestId(TestIds.breadcrumb.subgraph)
|
||||
.waitFor({ state: 'visible', timeout: 5000 })
|
||||
const homeBreadcrumb = comfyPage.page.getByRole('link', {
|
||||
name: 'subgraph-with-promoted-text-widget'
|
||||
})
|
||||
await homeBreadcrumb.waitFor({ state: 'visible' })
|
||||
await homeBreadcrumb.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Widget count should be reduced
|
||||
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
|
||||
expect(finalWidgetCount).toBeLessThan(initialWidgetCount)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -54,7 +54,10 @@ async function searchAndExpectResult(
|
||||
test.describe('Subgraph Search Aliases', { tag: ['@subgraph'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can set search aliases on subgraph and find via search', async ({
|
||||
|
||||
56
browser_tests/tests/templateFitView.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
/**
|
||||
* Tests that templates are automatically fitted to view when loaded.
|
||||
*
|
||||
* When openSource === 'template', fitView() is called to ensure
|
||||
* templates with saved off-screen viewport positions (extra.ds)
|
||||
* are always displayed correctly.
|
||||
*/
|
||||
test.describe('Template Fit View', { tag: ['@canvas', '@workflow'] }, () => {
|
||||
test('should automatically fit view when loading a template with off-screen saved position', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.EnableWorkflowViewRestore', true)
|
||||
|
||||
// Serialize the current default graph, inject an extreme off-screen
|
||||
// viewport position, then reload it as a template. Without the fix,
|
||||
// the saved offset [-5000, -5000] would be restored and nodes would
|
||||
// be invisible.
|
||||
const viewportState = await comfyPage.page.evaluate(async () => {
|
||||
const app = window.app!
|
||||
const workflow = app.graph.serialize()
|
||||
|
||||
workflow.extra = {
|
||||
...workflow.extra,
|
||||
ds: { scale: 1, offset: [-5000, -5000] }
|
||||
}
|
||||
|
||||
await app.loadGraphData(workflow as ComfyWorkflowJSON, true, true, null, {
|
||||
openSource: 'template'
|
||||
})
|
||||
|
||||
return {
|
||||
offsetX: app.canvas.ds.offset[0],
|
||||
offsetY: app.canvas.ds.offset[1],
|
||||
nodeCount: app.graph._nodes.length
|
||||
}
|
||||
})
|
||||
|
||||
expect(viewportState.nodeCount).toBeGreaterThan(0)
|
||||
|
||||
// fitView() should have overridden the saved [-5000, -5000] offset
|
||||
expect(
|
||||
viewportState.offsetX,
|
||||
'Viewport X offset should not be the saved off-screen value'
|
||||
).not.toBe(-5000)
|
||||
expect(
|
||||
viewportState.offsetY,
|
||||
'Viewport Y offset should not be the saved off-screen value'
|
||||
).not.toBe(-5000)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 115 KiB |
@@ -22,7 +22,6 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
|
||||
name: 'TestSettingsExtension',
|
||||
settings: [
|
||||
{
|
||||
// Extensions can register arbitrary setting IDs
|
||||
id: 'TestHiddenSetting' as TestSettingId,
|
||||
name: 'Test Hidden Setting',
|
||||
type: 'hidden',
|
||||
@@ -30,7 +29,6 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
|
||||
category: ['Test', 'Hidden']
|
||||
},
|
||||
{
|
||||
// Extensions can register arbitrary setting IDs
|
||||
id: 'TestDeprecatedSetting' as TestSettingId,
|
||||
name: 'Test Deprecated Setting',
|
||||
type: 'text',
|
||||
@@ -39,7 +37,6 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
|
||||
category: ['Test', 'Deprecated']
|
||||
},
|
||||
{
|
||||
// Extensions can register arbitrary setting IDs
|
||||
id: 'TestVisibleSetting' as TestSettingId,
|
||||
name: 'Test Visible Setting',
|
||||
type: 'text',
|
||||
@@ -52,238 +49,143 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
|
||||
})
|
||||
|
||||
test('can open settings dialog and use search box', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Find the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await expect(searchBox).toBeVisible()
|
||||
|
||||
// Verify search box has the correct placeholder
|
||||
await expect(searchBox).toHaveAttribute(
|
||||
await expect(dialog.searchBox).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search')
|
||||
)
|
||||
})
|
||||
|
||||
test('search box is functional and accepts input', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Find and interact with the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Comfy')
|
||||
|
||||
// Verify the input was accepted
|
||||
await expect(searchBox).toHaveValue('Comfy')
|
||||
await dialog.searchBox.fill('Comfy')
|
||||
await expect(dialog.searchBox).toHaveValue('Comfy')
|
||||
})
|
||||
|
||||
test('search box clears properly', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Find and interact with the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('test')
|
||||
await expect(searchBox).toHaveValue('test')
|
||||
await dialog.searchBox.fill('test')
|
||||
await expect(dialog.searchBox).toHaveValue('test')
|
||||
|
||||
// Clear the search box
|
||||
await searchBox.clear()
|
||||
await expect(searchBox).toHaveValue('')
|
||||
await dialog.searchBox.clear()
|
||||
await expect(dialog.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('settings categories are visible in sidebar', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Check that the sidebar has categories
|
||||
const categories = comfyPage.page.locator(
|
||||
'.settings-sidebar .p-listbox-option'
|
||||
)
|
||||
expect(await categories.count()).toBeGreaterThan(0)
|
||||
|
||||
// Check that at least one category is visible
|
||||
await expect(categories.first()).toBeVisible()
|
||||
expect(await dialog.categories.count()).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('can select different categories in sidebar', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Click on a specific category (Appearance) to verify category switching
|
||||
const appearanceCategory = comfyPage.page.getByRole('option', {
|
||||
name: 'Appearance'
|
||||
})
|
||||
await appearanceCategory.click()
|
||||
const categoryCount = await dialog.categories.count()
|
||||
|
||||
// Verify the category is selected
|
||||
await expect(appearanceCategory).toHaveClass(/p-listbox-option-selected/)
|
||||
})
|
||||
if (categoryCount > 1) {
|
||||
await dialog.categories.nth(1).click()
|
||||
|
||||
test('settings content area is visible', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Check that the content area is visible
|
||||
const contentArea = comfyPage.page.locator('.settings-content')
|
||||
await expect(contentArea).toBeVisible()
|
||||
|
||||
// Check that tab panels are visible
|
||||
const tabPanels = comfyPage.page.locator('.settings-tab-panels')
|
||||
await expect(tabPanels).toBeVisible()
|
||||
await expect(dialog.categories.nth(1)).toHaveClass(
|
||||
/bg-interface-menu-component-surface-selected/
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('search functionality affects UI state', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Find the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
|
||||
// Type in search box
|
||||
await searchBox.fill('graph')
|
||||
|
||||
// Verify that the search input is handled
|
||||
await expect(searchBox).toHaveValue('graph')
|
||||
await dialog.searchBox.fill('graph')
|
||||
await expect(dialog.searchBox).toHaveValue('graph')
|
||||
})
|
||||
|
||||
test('settings dialog can be closed', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Close with escape key
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(settingsDialog).not.toBeVisible()
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('search box has proper debouncing behavior', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Type rapidly in search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('a')
|
||||
await searchBox.fill('ab')
|
||||
await searchBox.fill('abc')
|
||||
await searchBox.fill('abcd')
|
||||
await dialog.searchBox.fill('a')
|
||||
await dialog.searchBox.fill('ab')
|
||||
await dialog.searchBox.fill('abc')
|
||||
await dialog.searchBox.fill('abcd')
|
||||
|
||||
// Verify final value
|
||||
await expect(searchBox).toHaveValue('abcd')
|
||||
await expect(dialog.searchBox).toHaveValue('abcd')
|
||||
})
|
||||
|
||||
test('search excludes hidden settings from results', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Test')
|
||||
await dialog.searchBox.fill('Test')
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Should show visible setting but not hidden setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
await expect(dialog.contentArea).toContainText('Test Visible Setting')
|
||||
await expect(dialog.contentArea).not.toContainText('Test Hidden Setting')
|
||||
})
|
||||
|
||||
test('search excludes deprecated settings from results', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Test')
|
||||
await dialog.searchBox.fill('Test')
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Should show visible setting but not deprecated setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
await expect(dialog.contentArea).toContainText('Test Visible Setting')
|
||||
await expect(dialog.contentArea).not.toContainText(
|
||||
'Test Deprecated Setting'
|
||||
)
|
||||
})
|
||||
|
||||
test('search shows visible settings but excludes hidden and deprecated', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Test')
|
||||
await dialog.searchBox.fill('Test')
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Should only show the visible setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
|
||||
// Should not show hidden or deprecated settings
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
await expect(dialog.contentArea).toContainText('Test Visible Setting')
|
||||
await expect(dialog.contentArea).not.toContainText('Test Hidden Setting')
|
||||
await expect(dialog.contentArea).not.toContainText(
|
||||
'Test Deprecated Setting'
|
||||
)
|
||||
})
|
||||
|
||||
test('search by setting name excludes hidden and deprecated', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const dialog = comfyPage.settingDialog
|
||||
await dialog.open()
|
||||
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
await dialog.searchBox.clear()
|
||||
await dialog.searchBox.fill('Hidden')
|
||||
await expect(dialog.contentArea).not.toContainText('Test Hidden Setting')
|
||||
|
||||
// Search specifically for hidden setting by name
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Hidden')
|
||||
await dialog.searchBox.clear()
|
||||
await dialog.searchBox.fill('Deprecated')
|
||||
await expect(dialog.contentArea).not.toContainText(
|
||||
'Test Deprecated Setting'
|
||||
)
|
||||
|
||||
// Should not show the hidden setting even when searching by name
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
|
||||
// Search specifically for deprecated setting by name
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Deprecated')
|
||||
|
||||
// Should not show the deprecated setting even when searching by name
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
|
||||
// Search for visible setting by name - should work
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Visible')
|
||||
|
||||
// Should show the visible setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await dialog.searchBox.clear()
|
||||
await dialog.searchBox.fill('Visible')
|
||||
await expect(dialog.contentArea).toContainText('Test Visible Setting')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
@@ -102,6 +102,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
// await comfyPage.setup()
|
||||
await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
@@ -928,7 +929,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
'Comfy.LinkRelease.ActionShift',
|
||||
'context menu'
|
||||
)
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
|
||||
const samplerNode = (
|
||||
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
@@ -994,6 +998,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
'Comfy.LinkRelease.ActionShift',
|
||||
'search box'
|
||||
)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
|
||||
const samplerNode = (
|
||||
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
@@ -1048,6 +1056,11 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
|
||||
// Setup workflow with a KSampler node
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.nodeOps.waitForGraphNodes(0)
|
||||
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,133 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
getPromotedWidgetNames,
|
||||
getPromotedWidgetCountByName
|
||||
} from '../../../../helpers/promotedWidgets'
|
||||
|
||||
test.describe('Vue Nodes Image Preview', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
})
|
||||
|
||||
async function loadImageOnNode(comfyPage: ComfyPage) {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const loadImageNode = (
|
||||
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
||||
)[0]
|
||||
const { x, y } = await loadImageNode.getPosition()
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFile('image64x64.webp', {
|
||||
dropPosition: { x, y }
|
||||
})
|
||||
|
||||
const imagePreview = comfyPage.page.locator('.image-preview')
|
||||
await expect(imagePreview).toBeVisible()
|
||||
await expect(imagePreview.locator('img')).toBeVisible()
|
||||
await expect(imagePreview).toContainText('x')
|
||||
|
||||
return {
|
||||
imagePreview,
|
||||
nodeId: String(loadImageNode.id)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(#8143): Re-enable after image preview sync is working in CI
|
||||
test.fixme('opens mask editor from image preview button', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { imagePreview } = await loadImageOnNode(comfyPage)
|
||||
|
||||
await imagePreview.locator('[role="img"]').focus()
|
||||
await comfyPage.page.getByLabel('Edit or mask image').click()
|
||||
|
||||
await expect(comfyPage.page.locator('.mask-editor-dialog')).toBeVisible()
|
||||
})
|
||||
|
||||
// TODO(#8143): Re-enable after image preview sync is working in CI
|
||||
test.fixme('shows image context menu options', async ({ comfyPage }) => {
|
||||
const { nodeId } = await loadImageOnNode(comfyPage)
|
||||
|
||||
const nodeHeader = comfyPage.vueNodes
|
||||
.getNodeLocator(nodeId)
|
||||
.locator('.lg-node-header')
|
||||
await nodeHeader.click()
|
||||
await nodeHeader.click({ button: 'right' })
|
||||
|
||||
const contextMenu = comfyPage.page.locator('.p-contextmenu')
|
||||
await expect(contextMenu).toBeVisible()
|
||||
await expect(contextMenu.getByText('Open Image')).toBeVisible()
|
||||
await expect(contextMenu.getByText('Copy Image')).toBeVisible()
|
||||
await expect(contextMenu.getByText('Save Image')).toBeVisible()
|
||||
await expect(contextMenu.getByText('Open in Mask Editor')).toBeVisible()
|
||||
})
|
||||
|
||||
test(
|
||||
'renders promoted image previews for each subgraph node',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-multiple-promoted-previews'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const firstSubgraphNode = comfyPage.vueNodes.getNodeLocator('7')
|
||||
const secondSubgraphNode = comfyPage.vueNodes.getNodeLocator('8')
|
||||
|
||||
await expect(firstSubgraphNode).toBeVisible()
|
||||
await expect(secondSubgraphNode).toBeVisible()
|
||||
|
||||
const firstPromotedWidgets = await getPromotedWidgetNames(comfyPage, '7')
|
||||
const secondPromotedWidgets = await getPromotedWidgetNames(comfyPage, '8')
|
||||
expect(firstPromotedWidgets).toEqual([
|
||||
'$$canvas-image-preview',
|
||||
'$$canvas-image-preview'
|
||||
])
|
||||
expect(secondPromotedWidgets).toEqual(['$$canvas-image-preview'])
|
||||
|
||||
expect(
|
||||
await getPromotedWidgetCountByName(
|
||||
comfyPage,
|
||||
'7',
|
||||
'$$canvas-image-preview'
|
||||
)
|
||||
).toBe(2)
|
||||
expect(
|
||||
await getPromotedWidgetCountByName(
|
||||
comfyPage,
|
||||
'8',
|
||||
'$$canvas-image-preview'
|
||||
)
|
||||
).toBe(1)
|
||||
|
||||
await expect(
|
||||
firstSubgraphNode.locator('.lg-node-widgets')
|
||||
).not.toContainText('$$canvas-image-preview')
|
||||
await expect(
|
||||
secondSubgraphNode.locator('.lg-node-widgets')
|
||||
).not.toContainText('$$canvas-image-preview')
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.Canvas.FitView')
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const firstPreviewImages = firstSubgraphNode.locator('.image-preview img')
|
||||
const secondPreviewImages =
|
||||
secondSubgraphNode.locator('.image-preview img')
|
||||
|
||||
await expect(firstPreviewImages).toHaveCount(2, { timeout: 30_000 })
|
||||
await expect(secondPreviewImages).toHaveCount(1, { timeout: 30_000 })
|
||||
|
||||
await expect(firstPreviewImages.first()).toBeVisible({ timeout: 30_000 })
|
||||
await expect(firstPreviewImages.nth(1)).toBeVisible({ timeout: 30_000 })
|
||||
await expect(secondPreviewImages.first()).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-multiple-promoted-previews.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
After Width: | Height: | Size: 93 KiB |
@@ -9,17 +9,20 @@ test.describe('Vue Upload Widgets', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test(
|
||||
'should hide canvas-only upload buttons',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.workflow.loadWorkflow('widgets/all_load_widgets')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.workflow.loadWorkflow('widgets/all_load_widgets')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-nodes-upload-widgets.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
await expect(
|
||||
comfyPage.page.getByText('choose file to upload', { exact: true })
|
||||
).not.toBeVisible()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.page.getByText('Error loading image').count())
|
||||
.toBeGreaterThan(0)
|
||||
await expect
|
||||
.poll(() => comfyPage.page.getByText('Error loading video').count())
|
||||
.toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
@@ -3,7 +3,6 @@
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/assets/css/style.css",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true,
|
||||
|
||||
@@ -36,6 +36,25 @@ When you must handle uncertain types, prefer these approaches in order:
|
||||
- Don't expose internal implementation types (e.g., Pinia store internals)
|
||||
- Reactive refs (`ComputedRef<T>`) should be unwrapped before exposing
|
||||
|
||||
## Avoiding Circular Dependencies
|
||||
|
||||
Extract type guards and their associated interfaces into **leaf modules** — files with only `import type` statements. This keeps them safe to import from anywhere without pulling in heavy transitive dependencies.
|
||||
|
||||
```typescript
|
||||
// ✅ myTypes.ts — leaf module (only type imports)
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
export interface MyView extends IBaseWidget {
|
||||
/* ... */
|
||||
}
|
||||
export function isMyView(w: IBaseWidget): w is MyView {
|
||||
return 'myProp' in w
|
||||
}
|
||||
|
||||
// ❌ myView.ts — heavy module (runtime imports from stores, utils, etc.)
|
||||
// Importing the type guard from here drags in the entire dependency tree.
|
||||
```
|
||||
|
||||
## Utility Libraries
|
||||
|
||||
- Use `es-toolkit` for utility functions (not lodash)
|
||||
|
||||
32
global.d.ts
vendored
@@ -5,9 +5,33 @@ declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
|
||||
interface ImpactQueueFunction {
|
||||
(...args: unknown[]): void
|
||||
a?: unknown[][]
|
||||
}
|
||||
|
||||
type GtagGetFieldName = 'client_id' | 'session_id' | 'session_number'
|
||||
|
||||
interface GtagGetFieldValueMap {
|
||||
client_id: string | number | undefined
|
||||
session_id: string | number | undefined
|
||||
session_number: string | number | undefined
|
||||
}
|
||||
|
||||
interface GtagFunction {
|
||||
<TField extends GtagGetFieldName>(
|
||||
command: 'get',
|
||||
targetId: string,
|
||||
fieldName: TField,
|
||||
callback: (value: GtagGetFieldValueMap[TField]) => void
|
||||
): void
|
||||
(...args: unknown[]): void
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__CONFIG__: {
|
||||
gtm_container_id?: string
|
||||
ga_measurement_id?: string
|
||||
mixpanel_token?: string
|
||||
require_whitelist?: boolean
|
||||
subscription_required?: boolean
|
||||
@@ -31,12 +55,10 @@ interface Window {
|
||||
badge?: string
|
||||
}
|
||||
}
|
||||
__ga_identity__?: {
|
||||
client_id?: string
|
||||
session_id?: string
|
||||
session_number?: string
|
||||
}
|
||||
dataLayer?: Array<Record<string, unknown>>
|
||||
gtag?: GtagFunction
|
||||
ire_o?: string
|
||||
ire?: ImpactQueueFunction
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
|
||||
42
index.html
@@ -35,18 +35,6 @@
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#vue-app:has(#loading-logo) {
|
||||
display: contents;
|
||||
color: var(--fg-color);
|
||||
& #loading-logo {
|
||||
place-self: center;
|
||||
font-size: clamp(2px, 1vw, 6px);
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
max-width: 100vw;
|
||||
border-radius: 20ch;
|
||||
}
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -65,36 +53,6 @@
|
||||
<body class="litegraph grid">
|
||||
<div id="vue-app">
|
||||
<span class="visually-hidden" role="status">Loading ComfyUI...</span>
|
||||
<svg
|
||||
width="520"
|
||||
height="520"
|
||||
viewBox="0 0 520 520"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="loading-logo"
|
||||
>
|
||||
<mask
|
||||
id="mask0_227_285"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="520"
|
||||
height="520"
|
||||
>
|
||||
<path
|
||||
d="M0 184.335C0 119.812 0 87.5502 12.5571 62.9055C23.6026 41.2274 41.2274 23.6026 62.9055 12.5571C87.5502 0 119.812 0 184.335 0H335.665C400.188 0 432.45 0 457.094 12.5571C478.773 23.6026 496.397 41.2274 507.443 62.9055C520 87.5502 520 119.812 520 184.335V335.665C520 400.188 520 432.45 507.443 457.094C496.397 478.773 478.773 496.397 457.094 507.443C432.45 520 400.188 520 335.665 520H184.335C119.812 520 87.5502 520 62.9055 507.443C41.2274 496.397 23.6026 478.773 12.5571 457.094C0 432.45 0 400.188 0 335.665V184.335Z"
|
||||
fill="#EEFF30"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_227_285)">
|
||||
<rect y="0.751831" width="520" height="520" fill="#172DD7" />
|
||||
<path
|
||||
d="M176.484 428.831C168.649 428.831 162.327 425.919 158.204 420.412C153.966 414.755 152.861 406.857 155.171 398.749L164.447 366.178C165.187 363.585 164.672 360.794 163.059 358.636C161.446 356.483 158.921 355.216 156.241 355.216H129.571C121.731 355.216 115.409 352.308 111.289 346.802C107.051 341.14 105.946 333.242 108.258 325.134L140.124 213.748L143.642 201.51C148.371 184.904 165.62 171.407 182.097 171.407H214.009C217.817 171.407 221.167 168.868 222.215 165.183L232.769 128.135C237.494 111.545 254.742 98.048 271.219 98.048L339.468 97.9264L389.431 97.9221C397.268 97.9221 403.59 100.831 407.711 106.337C411.949 111.994 413.054 119.892 410.744 128L396.457 178.164C391.734 194.75 374.485 208.242 358.009 208.242L289.607 208.372H257.706C253.902 208.372 250.557 210.907 249.502 214.588L222.903 307.495C222.159 310.093 222.673 312.892 224.291 315.049C225.904 317.202 228.428 318.469 231.107 318.469C231.113 318.469 276.307 318.381 276.307 318.381H326.122C333.959 318.381 340.281 321.29 344.402 326.796C348.639 332.457 349.744 340.355 347.433 348.463L333.146 398.619C328.423 415.209 311.174 428.701 294.698 428.701L226.299 428.831H176.484Z"
|
||||
fill="#F0FF41"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -20,10 +20,6 @@ const config: KnipConfig = {
|
||||
'packages/tailwind-utils': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
},
|
||||
'packages/design-system': {
|
||||
entry: ['src/**/*.ts'],
|
||||
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
|
||||
},
|
||||
'packages/registry-types': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
}
|
||||
@@ -31,6 +27,7 @@ const config: KnipConfig = {
|
||||
ignoreBinaries: ['python3', 'gh'],
|
||||
ignoreDependencies: [
|
||||
// Weird importmap things
|
||||
'@iconify-json/lucide',
|
||||
'@iconify/json',
|
||||
'@primeuix/forms',
|
||||
'@primeuix/styled',
|
||||
@@ -67,7 +64,8 @@ const config: KnipConfig = {
|
||||
},
|
||||
tags: [
|
||||
'-knipIgnoreUnusedButUsedByCustomNodes',
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch'
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch',
|
||||
'-knipIgnoreUsedByStackedPR'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,19 @@ export default {
|
||||
|
||||
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
]
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => {
|
||||
const commands = [...formatAndEslint(stagedFiles), 'pnpm typecheck']
|
||||
|
||||
const hasBrowserTestsChanges = stagedFiles
|
||||
.map((f) => path.relative(process.cwd(), f).replace(/\\/g, '/'))
|
||||
.some((f) => f.startsWith('browser_tests/'))
|
||||
|
||||
if (hasBrowserTestsChanges) {
|
||||
commands.push('pnpm typecheck:browser')
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames: string[]) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.39.9",
|
||||
"version": "1.40.10",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -193,7 +193,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "^8.0.0-beta.8"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"description": "Shared design system for ComfyUI Frontend",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./tailwind-config": "./tailwind.config.ts",
|
||||
"./css/*": "./src/css/*"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -12,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "catalog:",
|
||||
"@iconify/tailwind": "catalog:"
|
||||
"@iconify/tailwind4": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "catalog:",
|
||||
|
||||
@@ -7,11 +7,22 @@
|
||||
|
||||
@plugin 'tailwindcss-primeui';
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
@plugin "@iconify/tailwind4" {
|
||||
scale: 1.2;
|
||||
icon-sets: from-folder(comfy, './packages/design-system/src/icons');
|
||||
}
|
||||
|
||||
/* Safelist dynamic comfy icons for node library folders */
|
||||
@source inline("icon-[comfy--{ai-model,bfl,bria,bytedance,credits,extensions-blocks,file-output,gemini,grok,hitpaw,ideogram,image-ai-edit,kling,ltxv,luma,magnific,mask,meshy,minimax,moonvalley-marey,node,openai,pin,pixverse,play,recraft,rodin,runway,sora,stability-ai,template,tencent,topaz,tripo,veo,vidu,wan,wavespeed,workflow}]");
|
||||
|
||||
/* Safelist dynamic comfy icons for essential nodes (kebab-case of node names) */
|
||||
@source inline("icon-[comfy--{load-image,save-image,load-video,save-video,load-3-d,save-glb,image-batch,image-crop,image-scale,image-rotate,image-blur,image-invert,canny,recraft-remove-background-node,kling-lip-sync-audio-to-video-node,load-audio,save-audio,stability-text-to-audio,lora-loader,clip-text-encode,get-video-components,tencent-text-to-model-node,tencent-image-to-model-node,open-ai-chat-node,subgraph-blueprint-canny-to-video-ltx-2-0,subgraph-blueprint-pose-to-video-ltx-2-0}]");
|
||||
|
||||
@custom-variant touch (@media (hover: none));
|
||||
|
||||
@theme {
|
||||
--shadow-interface: var(--interface-panel-box-shadow);
|
||||
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
@@ -598,6 +609,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
@utility bg-subscription-gradient {
|
||||
background: var(--color-subscription-button-gradient);
|
||||
}
|
||||
|
||||
@utility scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
@@ -1181,7 +1196,7 @@ button.comfy-queue-btn {
|
||||
.graphdialog {
|
||||
min-height: 1em;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */
|
||||
z-index: 1500;
|
||||
}
|
||||
|
||||
.graphdialog .name {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { existsSync, readFileSync, readdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const fileName = fileURLToPath(import.meta.url)
|
||||
const dirName = dirname(fileName)
|
||||
const customIconsPath = join(dirName, 'icons')
|
||||
|
||||
// Iconify collection structure
|
||||
interface IconifyIcon {
|
||||
body: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
interface IconifyCollection {
|
||||
prefix: string
|
||||
icons: Record<string, IconifyIcon>
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
// Create an Iconify collection for custom icons
|
||||
export const iconCollection: IconifyCollection = {
|
||||
prefix: 'comfy',
|
||||
icons: {},
|
||||
width: 16,
|
||||
height: 16
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an SVG file contains valid SVG content
|
||||
*/
|
||||
function validateSvgContent(content: string, filename: string): void {
|
||||
if (!content.trim()) {
|
||||
throw new Error(`Empty SVG file: ${filename}`)
|
||||
}
|
||||
|
||||
if (!content.includes('<svg')) {
|
||||
throw new Error(`Invalid SVG file (missing <svg> tag): ${filename}`)
|
||||
}
|
||||
|
||||
// Basic XML structure validation
|
||||
const openTags = (content.match(/<svg[^>]*>/g) || []).length
|
||||
const closeTags = (content.match(/<\/svg>/g) || []).length
|
||||
|
||||
if (openTags !== closeTags) {
|
||||
throw new Error(`Malformed SVG file (mismatched svg tags): ${filename}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads custom SVG icons from the icons directory
|
||||
*/
|
||||
function loadCustomIcons(): void {
|
||||
if (!existsSync(customIconsPath)) {
|
||||
console.warn(`Custom icons directory not found: ${customIconsPath}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const files = readdirSync(customIconsPath)
|
||||
const svgFiles = files.filter((file) => file.endsWith('.svg'))
|
||||
|
||||
if (svgFiles.length === 0) {
|
||||
console.warn('No SVG files found in custom icons directory')
|
||||
return
|
||||
}
|
||||
|
||||
svgFiles.forEach((file) => {
|
||||
const name = file.replace('.svg', '')
|
||||
const filePath = join(customIconsPath, file)
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
validateSvgContent(content, file)
|
||||
|
||||
iconCollection.icons[name] = {
|
||||
body: content
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load custom icon ${file}:`,
|
||||
error instanceof Error ? error.message : error
|
||||
)
|
||||
// Continue loading other icons instead of failing the entire build
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to read custom icons directory:',
|
||||
error instanceof Error ? error.message : error
|
||||
)
|
||||
// Don't throw here - allow build to continue without custom icons
|
||||
}
|
||||
}
|
||||
|
||||
// Load icons when this module is imported
|
||||
loadCustomIcons()
|
||||
@@ -251,26 +251,25 @@ Icons are automatically imported using `unplugin-icons` - no manual imports need
|
||||
|
||||
The icon system has two layers:
|
||||
|
||||
1. **Build-time Processing** (`packages/design-system/src/iconCollection.ts`):
|
||||
- Scans `packages/design-system/src/icons/` for SVG files
|
||||
- Validates SVG content and structure
|
||||
- Creates Iconify collection for Tailwind CSS
|
||||
- Provides error handling for malformed files
|
||||
1. **Tailwind CSS Plugin** (`@iconify/tailwind4`):
|
||||
- Configured via `@plugin` directive in `packages/design-system/src/css/style.css`
|
||||
- Uses `from-folder(comfy, ...)` to load SVGs from `packages/design-system/src/icons/`
|
||||
- Auto-cleans and optimizes SVGs at build time
|
||||
|
||||
2. **Vite Runtime** (`vite.config.mts`):
|
||||
- Enables direct SVG import as Vue components
|
||||
- Supports dynamic icon loading
|
||||
|
||||
```typescript
|
||||
// Build script creates Iconify collection
|
||||
export const iconCollection: IconifyCollection = {
|
||||
prefix: 'comfy',
|
||||
icons: {
|
||||
workflow: { body: '<svg>...</svg>' },
|
||||
node: { body: '<svg>...</svg>' }
|
||||
}
|
||||
```css
|
||||
/* CSS configuration for Tailwind icon classes */
|
||||
@plugin "@iconify/tailwind4" {
|
||||
prefix: 'icon';
|
||||
scale: 1.2;
|
||||
icon-sets: from-folder(comfy, './packages/design-system/src/icons');
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Vite configuration for component-based usage
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
|
||||
10
packages/design-system/src/icons/bfl.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1471_12672)">
|
||||
<path d="M365.047 229.802H310.584L256.118 153.072L86.2157 392.168H140.796L256.115 229.807H310.581L195.261 392.168H249.994L365.047 229.802L512 436.698H470.893V436.7H426.019V392.343L365.047 306.532L304.415 392.178V436.698H163.632L163.629 436.703H109.164L109.167 436.698H-0.105347L256.118 76L365.047 229.802Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1471_12672">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
18
packages/design-system/src/icons/bria.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1471_12667)">
|
||||
<g clip-path="url(#clip1_1471_12667)">
|
||||
<path d="M0.307203 236.902C1.1008 227.43 4.48 211.021 6.5792 201.139C23.5008 121.805 83.0208 46.1824 160.691 20.3776V389.734H411.443C419.84 389.734 446.259 381.158 455.168 377.446C468.685 371.814 480.691 363.648 491.597 354.074C465.357 424.038 401.024 480.896 329.242 501.094C314.01 505.37 290.611 510.694 275.226 512H239.59C223.667 508.16 207.155 507.136 191.206 503.117C103.936 481.152 29.7984 406.63 8.704 318.925C5.0176 303.59 4.0704 287.744 0.307203 272.538C1.024 260.89 -0.665597 248.397 0.307203 236.877V236.902Z" fill="#8E4CFF"/>
|
||||
<path d="M265.062 211.43H375.808C378.394 211.43 392.55 217.498 395.75 219.494C427.213 238.925 420.122 291.226 384.794 301.952C382.771 302.566 366.669 305.69 365.619 305.69H268.877L265.062 301.875V211.456V211.43Z" fill="#8E4CFF"/>
|
||||
<path d="M265.062 137.549V48.4096H373.248C374.861 48.4096 384.205 53.8624 386.611 55.424C406.528 68.3776 414.49 96.5376 401.152 117.069C398.106 121.754 380.339 137.574 375.808 137.574H265.062V137.549Z" fill="#8E4CFF"/>
|
||||
<path d="M489.062 147.738C496.128 167.706 505.523 187.264 506.906 208.845L491.674 192.282C477.773 180.736 460.928 174.003 443.29 170.624L489.062 147.738Z" fill="#8E4CFF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1471_12667">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1471_12667">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
13
packages/design-system/src/icons/bytedance.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1471_12658)">
|
||||
<path d="M324.094 389.858L284.667 379.567V191.5L326.871 180.816C350.01 174.941 369.446 170.154 370.371 170.339C371.112 170.339 371.667 222.027 371.667 285.326V400.334L367.594 400.15C365.189 400.15 345.566 395.361 324.094 389.835V389.857V389.858Z" fill="#00C8D2"/>
|
||||
<path d="M138.667 343.325C138.667 279.602 139.229 227.339 140.166 227.339C140.914 227.154 160.573 231.998 184.164 237.913L226.667 248.65L226.292 342.975L225.73 437.278L187.535 447.107C166.565 452.463 146.906 457.47 144.097 458.029L138.667 459.334V343.325Z" fill="#3C8CFF"/>
|
||||
<path d="M423.667 248.299C423.667 38.7081 423.853 27.4506 427.037 28.3797C428.722 28.9368 445.386 33.1843 463.921 37.8029C482.458 42.6075 500.807 47.2031 504.739 48.1312L511.667 49.9884L511.293 248.67L510.731 447.539L472.722 457.148C451.939 462.486 432.279 467.291 429.284 468.057L423.667 469.334V248.299Z" fill="#78E6DC"/>
|
||||
<path d="M-0.333038 248.845C-0.333038 140.208 0.222275 51.334 1.14852 51.334C1.88822 51.334 21.3242 56.1412 44.4631 61.8769L86.667 72.583V248.66C86.667 345.267 86.296 424.55 85.9262 424.55C85.3709 424.55 65.7494 429.544 42.4262 435.466L-0.333038 446.334V248.823V248.844V248.845Z" fill="#325AB4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1471_12658">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
packages/design-system/src/icons/canny.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
5
packages/design-system/src/icons/canny_to_image.svg
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
3
packages/design-system/src/icons/clip-text-encode.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M34 3.11111V6.07407C34 6.68795 33.4163 7.18519 32.6956 7.18519C31.975 7.18519 31.3913 6.68795 31.3913 6.07407V4.22222H25.3042V19.7778H28.3479C29.0685 19.7778 29.6523 20.275 29.6523 20.8889C29.6523 21.5028 29.0685 22 28.3479 22H19.6521C18.9315 22 18.3477 21.5028 18.3477 20.8889C18.3477 20.275 18.9315 19.7778 19.6521 19.7778H22.6958V4.22222H16.6087V6.07407C16.6087 6.68795 16.025 7.18519 15.3044 7.18519C14.5837 7.18519 14 6.68795 14 6.07407V3.11111C14 2.49723 14.5837 2 15.3044 2H32.6959C33.4166 2 34 2.49723 34 3.11111Z" fill="#8A8A8A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 652 B |
4
packages/design-system/src/icons/comfy-c.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 9">
|
||||
<path d="M1.82148 8.68376C1.61587 8.68376 1.44996 8.60733 1.34177 8.46284C1.23057 8.31438 1.20157 8.10711 1.26219 7.89434L1.50561 7.03961C1.52502 6.97155 1.51151 6.89831 1.46918 6.8417C1.42684 6.7852 1.3606 6.75194 1.29025 6.75194H0.590376C0.384656 6.75194 0.21875 6.67562 0.110614 6.53113C-0.000591531 6.38256 -0.0295831 6.17529 0.0310774 5.96252L0.867308 3.03952L0.959638 2.71838C1.08375 2.28258 1.53638 1.9284 1.96878 1.9284H2.80622C2.90615 1.9284 2.99406 1.86177 3.02157 1.76508L3.29852 0.79284C3.4225 0.357484 3.87514 0.0033043 4.30753 0.0033043L6.09854 0.000112775L7.40967 0C7.61533 0 7.78124 0.0763259 7.88937 0.220813C8.00058 0.369269 8.02957 0.576538 7.96895 0.78931L7.59405 2.10572C7.4701 2.54096 7.01746 2.89503 6.58507 2.89503L4.79008 2.89844H3.95292C3.8531 2.89844 3.7653 2.96496 3.73762 3.06155L3.03961 5.49964C3.02008 5.56781 3.03359 5.64127 3.07604 5.69787C3.11837 5.75437 3.18461 5.78763 3.2549 5.78763C3.25507 5.78763 4.44105 5.78532 4.44105 5.78532H5.7483C5.95396 5.78532 6.11986 5.86164 6.228 6.00613C6.33921 6.1547 6.3682 6.36197 6.30754 6.57474L5.93263 7.89092C5.80869 8.32628 5.35605 8.68034 4.92366 8.68034L3.12872 8.68376H1.82148Z" fill="#8A8A8A"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
5
packages/design-system/src/icons/compare_images.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.9316 2.13269H25.2057V3.57269H29.9316C31.1241 3.57269 32.0916 4.54018 32.0916 5.73269V18.0646C32.0916 19.2571 31.1241 20.2246 29.9316 20.2246H25.2057V21.6646H29.9316C30.8859 21.6646 31.8019 21.2849 32.4768 20.6099C33.1516 19.9349 33.5314 19.019 33.5314 18.0647V5.73281C33.5314 4.77843 33.1518 3.86249 32.4768 3.18761C31.8018 2.51273 30.8858 2.13293 29.9316 2.13293V2.13269Z" fill="#8A8A8A"/>
|
||||
<path d="M30.2025 15.7602C30.5531 15.7602 30.8738 15.5652 31.0341 15.2539C31.1953 14.9427 31.1691 14.5677 30.9656 14.2817L29.0269 11.5601L26.7994 8.44014C26.6231 8.19359 26.3391 8.04733 26.0363 8.04733C25.7335 8.04733 25.4494 8.19358 25.2731 8.44014L25.2056 8.53389V15.7601L30.2025 15.7602Z" fill="#8A8A8A"/>
|
||||
<path d="M23.0457 1.00018C22.6482 1.00018 22.3257 1.32269 22.3257 1.72018V2.13269H17.5999C16.6455 2.13269 15.7296 2.51237 15.0547 3.18737C14.3798 3.86237 14 4.77831 14 5.73257V18.0645C14 19.0189 14.3797 19.9348 15.0547 20.6097C15.7297 21.2846 16.6456 21.6644 17.5999 21.6644H22.3257V21.88C22.3257 22.2775 22.6482 22.6 23.0457 22.6C23.4432 22.6 23.7657 22.2775 23.7657 21.88V1.72C23.7657 1.32251 23.4432 1.00018 23.0457 1.00018ZM22.3257 15.7602H17.3289C16.9783 15.7602 16.6577 15.5652 16.4974 15.2539C16.3361 14.9427 16.3624 14.5677 16.5658 14.2817L17.4471 13.0433L18.6208 11.397H18.6199C18.7961 11.1495 19.0802 11.0033 19.383 11.0033C19.6867 11.0033 19.9708 11.1495 20.1471 11.397L21.318 13.0433L21.6517 13.5233L22.3258 12.568L22.3257 15.7602Z" fill="#8A8A8A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |