Compare commits
512 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0cc5eac95 | ||
|
|
feeb1d19c8 | ||
|
|
920266e1ff | ||
|
|
b81ccc0507 | ||
|
|
f4c523f188 | ||
|
|
cce5ade578 | ||
|
|
3767749398 | ||
|
|
c3ceef0461 | ||
|
|
ac952fbee3 | ||
|
|
72f7c3101d | ||
|
|
ca9627cada | ||
|
|
c8f62cd963 | ||
|
|
9900e46b95 | ||
|
|
c433b38ba8 | ||
|
|
0b501d7a1c | ||
|
|
0e197261b0 | ||
|
|
89bf5c182d | ||
|
|
e80c75482e | ||
|
|
569a131624 | ||
|
|
b1d02c6a7b | ||
|
|
abeafd5625 | ||
|
|
0906d7873a | ||
|
|
0ce3f1ecb0 | ||
|
|
ec6919c5ab | ||
|
|
10de690ccf | ||
|
|
cb3e4b5ed7 | ||
|
|
2b4ac582d4 | ||
|
|
7f60811ebf | ||
|
|
90e94df1e4 | ||
|
|
5b72fc7fdc | ||
|
|
601b739f75 | ||
|
|
a441908db3 | ||
|
|
a376503a7b | ||
|
|
69a4a17848 | ||
|
|
22f9ad31dc | ||
|
|
2a02159355 | ||
|
|
9258098e0c | ||
|
|
fa48fe587d | ||
|
|
7b64bfa173 | ||
|
|
59b62eadf1 | ||
|
|
98912efeb7 | ||
|
|
c7303ea465 | ||
|
|
618b451f1e | ||
|
|
a99f63784d | ||
|
|
6f7c7f3c61 | ||
|
|
4f049b66a2 | ||
|
|
458ff350c8 | ||
|
|
fc8c10bca4 | ||
|
|
28ab86cd4a | ||
|
|
efb6f8afe3 | ||
|
|
a2549f23c7 | ||
|
|
7877c96ffe | ||
|
|
4997d13219 | ||
|
|
c5135afd29 | ||
|
|
8663340067 | ||
|
|
a1ffc521a4 | ||
|
|
f7dce3116a | ||
|
|
f23530afcf | ||
|
|
c5f77de1bf | ||
|
|
c60645d9f4 | ||
|
|
ebbff358a0 | ||
|
|
a39c04d56e | ||
|
|
9e61d1509f | ||
|
|
0c511d08e6 | ||
|
|
929e3a5733 | ||
|
|
75c98d00fc | ||
|
|
7bce5ac4d0 | ||
|
|
e7733d94f9 | ||
|
|
20a3a95df2 | ||
|
|
d2bbf41fb5 | ||
|
|
97ccc6a854 | ||
|
|
b48619878f | ||
|
|
ac6130a556 | ||
|
|
86797d2492 | ||
|
|
112a693f3d | ||
|
|
57701f6145 | ||
|
|
c2145b6f45 | ||
|
|
0b91d53c9f | ||
|
|
b4ccaf4fec | ||
|
|
2d1b2cb1cc | ||
|
|
aa04ab78c1 | ||
|
|
1671437fb3 | ||
|
|
9d49cb0e4b | ||
|
|
43548785b5 | ||
|
|
b6038128cb | ||
|
|
a76159e9a0 | ||
|
|
a0ff78dbdb | ||
|
|
059dfcbebb | ||
|
|
c17d44638e | ||
|
|
0e385c4262 | ||
|
|
da98b1c4cc | ||
|
|
26d2e5de31 | ||
|
|
b0e37036d5 | ||
|
|
b7ddb47d2a | ||
|
|
6eb5ee99d8 | ||
|
|
6409e17d4a | ||
|
|
a6549fb41e | ||
|
|
06d5064b7c | ||
|
|
fd621f485e | ||
|
|
517ae56763 | ||
|
|
ae26390776 | ||
|
|
aa68422e0f | ||
|
|
dac2a2ec86 | ||
|
|
5cee4d828f | ||
|
|
d6247d69ce | ||
|
|
e2fa1e65d1 | ||
|
|
55c04b5533 | ||
|
|
540e785424 | ||
|
|
30a92d40eb | ||
|
|
679405e0a2 | ||
|
|
6cdc524174 | ||
|
|
80a42ebf10 | ||
|
|
c45f03f5e7 | ||
|
|
36c2604639 | ||
|
|
73f50e7e0b | ||
|
|
418fddd669 | ||
|
|
e3cda0e749 | ||
|
|
30465f17e0 | ||
|
|
5206939c78 | ||
|
|
73396784a8 | ||
|
|
3745d8d791 | ||
|
|
735153886f | ||
|
|
d04cc4e272 | ||
|
|
473aa120eb | ||
|
|
7986aebf27 | ||
|
|
2caa87d35d | ||
|
|
dabcd4741b | ||
|
|
e893f3ed03 | ||
|
|
1856479de9 | ||
|
|
fc69129a2f | ||
|
|
d953b8fa46 | ||
|
|
1847db7a47 | ||
|
|
9cbcda20a7 | ||
|
|
0fe0aea242 | ||
|
|
08ae36818a | ||
|
|
da6b4d2872 | ||
|
|
8e293b41f5 | ||
|
|
d2771a7a1d | ||
|
|
13869da300 | ||
|
|
6ba53f3af1 | ||
|
|
b23cebcba4 | ||
|
|
30dfe76577 | ||
|
|
c1f0cfe366 | ||
|
|
d1279fa474 | ||
|
|
a56462fc7c | ||
|
|
646bcf595b | ||
|
|
4796677a0a | ||
|
|
01ac9d9336 | ||
|
|
89a9288154 | ||
|
|
17b7ef18d6 | ||
|
|
1afc584393 | ||
|
|
9d71cdf8ef | ||
|
|
1c7f3e865a | ||
|
|
9ef40189f9 | ||
|
|
5c6eecd660 | ||
|
|
9b07993e1a | ||
|
|
c35d29f31c | ||
|
|
e50d7c5eef | ||
|
|
7c2cce40de | ||
|
|
2017b9016b | ||
|
|
0bf30e7621 | ||
|
|
7f5b685c9f | ||
|
|
ec824579d6 | ||
|
|
9e565154a9 | ||
|
|
541335bb31 | ||
|
|
814c4b8ef0 | ||
|
|
df3fff5dbb | ||
|
|
b0085114d7 | ||
|
|
7f9c70386f | ||
|
|
578870d345 | ||
|
|
05fab91bda | ||
|
|
5191e11650 | ||
|
|
92079a653e | ||
|
|
a7d14eb815 | ||
|
|
9aea6eae70 | ||
|
|
88a42172c5 | ||
|
|
e79013dcfe | ||
|
|
08f3370828 | ||
|
|
c4d3c672ad | ||
|
|
39eaa2e850 | ||
|
|
2d022e4e49 | ||
|
|
1ac6d6529f | ||
|
|
86fec820ac | ||
|
|
030d5845db | ||
|
|
dd1c878fdf | ||
|
|
3942603a38 | ||
|
|
244578db96 | ||
|
|
6b6edfde9f | ||
|
|
c54b675a48 | ||
|
|
b7008dfc5c | ||
|
|
d0ad4af51c | ||
|
|
4a182014e1 | ||
|
|
46cd522384 | ||
|
|
c977667a15 | ||
|
|
d531bc34c4 | ||
|
|
adfbec2744 | ||
|
|
23521559bf | ||
|
|
51f57aba17 | ||
|
|
97bab053df | ||
|
|
c1c5573e7f | ||
|
|
16d2a95760 | ||
|
|
f97b673481 | ||
|
|
c8d5a6f154 | ||
|
|
3708afaf21 | ||
|
|
43c23e526c | ||
|
|
a80eb84df1 | ||
|
|
f89898b3d0 | ||
|
|
af21142602 | ||
|
|
4b91860227 | ||
|
|
e53bafbca6 | ||
|
|
e01c8f06c7 | ||
|
|
c61ed4da37 | ||
|
|
4a4d6d070a | ||
|
|
4bedd873a1 | ||
|
|
f8bd910e63 | ||
|
|
1160231b62 | ||
|
|
a51e27bedf | ||
|
|
abed0656af | ||
|
|
5febda16c7 | ||
|
|
069dc67c30 | ||
|
|
7623810166 | ||
|
|
21fa88461f | ||
|
|
27b0493306 | ||
|
|
ad2c1a0d3e | ||
|
|
f51866d988 | ||
|
|
46627bb44b | ||
|
|
68cadbda9f | ||
|
|
0f2260065a | ||
|
|
4007cc13c2 | ||
|
|
3920210c5c | ||
|
|
4e22bffae2 | ||
|
|
462a131557 | ||
|
|
ec01a04786 | ||
|
|
4c48241e19 | ||
|
|
886c40a69a | ||
|
|
479d1b28c7 | ||
|
|
c41b57128a | ||
|
|
5d178a407d | ||
|
|
73b7606f6e | ||
|
|
94f5031f0d | ||
|
|
c857e7d98c | ||
|
|
d5b8a555d9 | ||
|
|
f34d50da3d | ||
|
|
4f3693e322 | ||
|
|
431ad7d27f | ||
|
|
0c97b09a5a | ||
|
|
bdb9f0d845 | ||
|
|
77b85acdd5 | ||
|
|
8906f5c26e | ||
|
|
81194cc7fe | ||
|
|
f4b972fab5 | ||
|
|
3aa1c03566 | ||
|
|
600b7f93e5 | ||
|
|
2a7df57404 | ||
|
|
6352cd86ee | ||
|
|
0058691579 | ||
|
|
1531bb6d9f | ||
|
|
40245aacf9 | ||
|
|
6e49685f58 | ||
|
|
946823ce6c | ||
|
|
c05f1465db | ||
|
|
88164bdac5 | ||
|
|
fc9e347055 | ||
|
|
6fbf1248f4 | ||
|
|
56848724cd | ||
|
|
26c3eeb942 | ||
|
|
a8f869337e | ||
|
|
7e245ba1cf | ||
|
|
2a93f873b4 | ||
|
|
f8e7058e19 | ||
|
|
8d4e740baa | ||
|
|
3273ee938b | ||
|
|
94f1bc3b38 | ||
|
|
d5ce140eb6 | ||
|
|
b5f0c4bf73 | ||
|
|
545a990365 | ||
|
|
71e4a42cfe | ||
|
|
16b0ebf75a | ||
|
|
eaeb17bdc7 | ||
|
|
239b464957 | ||
|
|
00b6d989ec | ||
|
|
c5f05b1855 | ||
|
|
6fefcaad7b | ||
|
|
22fdfd7f0b | ||
|
|
6842eb05de | ||
|
|
37e7994d55 | ||
|
|
399893bbb2 | ||
|
|
227db065f3 | ||
|
|
b4352bcd8d | ||
|
|
39bab9d9e2 | ||
|
|
c71644f02f | ||
|
|
6aad7ee8b6 | ||
|
|
2b96d831fc | ||
|
|
dde0291add | ||
|
|
8af016ffc1 | ||
|
|
82b4547d7d | ||
|
|
791a25637f | ||
|
|
b922aa5c7c | ||
|
|
cbaebbc9c2 | ||
|
|
86b2e1aa6c | ||
|
|
61c5f05126 | ||
|
|
dde9c3dad5 | ||
|
|
ee5c127146 | ||
|
|
acba6097e0 | ||
|
|
82d00a1bcf | ||
|
|
b9224464c0 | ||
|
|
fba9a03df3 | ||
|
|
2fd624cd3d | ||
|
|
095fe2a175 | ||
|
|
d838777e04 | ||
|
|
7e0d1d441d | ||
|
|
ddab149f16 | ||
|
|
a73fdcd3bd | ||
|
|
d6e0c197bd | ||
|
|
3117d0fdc1 | ||
|
|
96fda64b70 | ||
|
|
e3d2c3a814 | ||
|
|
1a8900de1f | ||
|
|
05ba526388 | ||
|
|
4bc79181ae | ||
|
|
feafbf9cbf | ||
|
|
40f9b881f3 | ||
|
|
8236163fea | ||
|
|
59b555b448 | ||
|
|
71eeee6744 | ||
|
|
1ff6e27d9c | ||
|
|
64ef0f18b1 | ||
|
|
73bdbddf90 | ||
|
|
a55833b3a6 | ||
|
|
43012eb1d1 | ||
|
|
d1e019589d | ||
|
|
7bc79edf3d | ||
|
|
58ad01adfe | ||
|
|
5f1a9659e9 | ||
|
|
6c6c356c78 | ||
|
|
893fd498df | ||
|
|
1ca388457d | ||
|
|
69f0da06f8 | ||
|
|
d9a34872c3 | ||
|
|
31fac3873c | ||
|
|
8dc057517f | ||
|
|
4617e0fb1a | ||
|
|
f8ec87ddea | ||
|
|
c12f059940 | ||
|
|
cc320e0f84 | ||
|
|
acbc38ced4 | ||
|
|
777a6d9ce3 | ||
|
|
7e0b87dd32 | ||
|
|
0161a670cf | ||
|
|
0eba49c536 | ||
|
|
1d9c3f00b7 | ||
|
|
904408de01 | ||
|
|
700336fcc7 | ||
|
|
dd192777b7 | ||
|
|
6b6992591b | ||
|
|
45380f7ca0 | ||
|
|
f0b735f3dd | ||
|
|
9568d63820 | ||
|
|
073638672d | ||
|
|
8ae9210298 | ||
|
|
daf94d74d5 | ||
|
|
14b3d4c766 | ||
|
|
40880dbb59 | ||
|
|
aa4742e394 | ||
|
|
0a7000328a | ||
|
|
da7a49bb5c | ||
|
|
5e4439b905 | ||
|
|
ea0883271e | ||
|
|
3d303c7693 | ||
|
|
9f14edaf2b | ||
|
|
d1738b50d2 | ||
|
|
c560628f1f | ||
|
|
c56533bb23 | ||
|
|
1387d7e627 | ||
|
|
16f2e56d8e | ||
|
|
75ffab2160 | ||
|
|
4c19e1ba3a | ||
|
|
2161ae4e5b | ||
|
|
3148c90e28 | ||
|
|
497b2fba8d | ||
|
|
09d5e29f01 | ||
|
|
56b63ebab5 | ||
|
|
3ba776e6ca | ||
|
|
0a784d9236 | ||
|
|
00df7b428f | ||
|
|
27bacc36d4 | ||
|
|
394df49208 | ||
|
|
38847e1079 | ||
|
|
dd86417177 | ||
|
|
1366c8cb44 | ||
|
|
3a910f25e9 | ||
|
|
cc420b70a5 | ||
|
|
caa3ac2068 | ||
|
|
8baaf380dc | ||
|
|
d719a4e0fb | ||
|
|
d254559e20 | ||
|
|
d701758663 | ||
|
|
a11b78d1c3 | ||
|
|
dfb695be72 | ||
|
|
2974b9257a | ||
|
|
d11d07334b | ||
|
|
0c8fe41b84 | ||
|
|
ed0592d6e0 | ||
|
|
94f4147f92 | ||
|
|
67ee8726ef | ||
|
|
e48c78541c | ||
|
|
bf7a9bf5eb | ||
|
|
3fb2d423ba | ||
|
|
74f7311585 | ||
|
|
97c38583e9 | ||
|
|
324eff93fd | ||
|
|
4b1104f52c | ||
|
|
d702fc81a2 | ||
|
|
2a94ab4423 | ||
|
|
37f6c89383 | ||
|
|
35fab0bef3 | ||
|
|
795e932b8f | ||
|
|
8dddffe840 | ||
|
|
1f91a88d7b | ||
|
|
10f43be911 | ||
|
|
87517daf1f | ||
|
|
739ebd3d04 | ||
|
|
4582c71583 | ||
|
|
757f0ced81 | ||
|
|
a471a3f302 | ||
|
|
5a3a8d32ab | ||
|
|
44b109a449 | ||
|
|
229896a4b7 | ||
|
|
997b5ee819 | ||
|
|
ba99eca700 | ||
|
|
82c369322d | ||
|
|
546c5dabc8 | ||
|
|
7d450adf93 | ||
|
|
eed92864f2 | ||
|
|
8fd7852740 | ||
|
|
2a927bb9ea | ||
|
|
c566491ac7 | ||
|
|
7729611a2a | ||
|
|
8861492655 | ||
|
|
fa9d944b32 | ||
|
|
880437f3c0 | ||
|
|
c1e960c83c | ||
|
|
571386c061 | ||
|
|
44d886a18b | ||
|
|
92ac403679 | ||
|
|
dc3dab4e1c | ||
|
|
386594554e | ||
|
|
02a951ad58 | ||
|
|
92f0f4a21c | ||
|
|
645897f8b8 | ||
|
|
ef4179a06c | ||
|
|
a3e4af40c1 | ||
|
|
1dedce5ec6 | ||
|
|
3a4b36fb31 | ||
|
|
25457d31d4 | ||
|
|
4f9dc830b6 | ||
|
|
12d421b42c | ||
|
|
16ebfd6171 | ||
|
|
59c999324e | ||
|
|
624bcc75ab | ||
|
|
54e833502a | ||
|
|
c3242711c7 | ||
|
|
17391e4aad | ||
|
|
48b840a88d | ||
|
|
377fed584f | ||
|
|
14a6687cc9 | ||
|
|
d142893244 | ||
|
|
957a767ed0 | ||
|
|
3553c8e0d4 | ||
|
|
05221f7961 | ||
|
|
d113072a64 | ||
|
|
afa619b7df | ||
|
|
e25bbc19cb | ||
|
|
0251bc9e6c | ||
|
|
7a3f20c57d | ||
|
|
269fc7c8c9 | ||
|
|
db08f74d6a | ||
|
|
5db757ade2 | ||
|
|
59c03d2de5 | ||
|
|
b655c5544d | ||
|
|
9388ee0705 | ||
|
|
f228ec29eb | ||
|
|
7239e94092 | ||
|
|
e33a5f7736 | ||
|
|
c1c990e6f3 | ||
|
|
a890756868 | ||
|
|
015ee2df15 | ||
|
|
c3b2697568 | ||
|
|
dfcabd2834 | ||
|
|
634196cbd6 | ||
|
|
5611e90fda | ||
|
|
c23d95f8f9 | ||
|
|
c2377b62ac | ||
|
|
f328d4cd81 | ||
|
|
fbc1482b90 | ||
|
|
90e07af4f5 | ||
|
|
014c3f3172 | ||
|
|
419009424b | ||
|
|
f599c9bcb8 | ||
|
|
6c696cddb9 | ||
|
|
d787c21f8b | ||
|
|
f96f08be32 | ||
|
|
8ebb51b9a3 | ||
|
|
60e1b82df6 | ||
|
|
459afa158c | ||
|
|
1c3d3b33f6 | ||
|
|
f64365915b | ||
|
|
ec8e6f79b3 | ||
|
|
b89f467983 | ||
|
|
009dbcf8c7 | ||
|
|
4413fd248c | ||
|
|
8962597e69 |
@@ -6,6 +6,11 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
|
||||
# Note: localhost:8188 does not work.
|
||||
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
|
||||
|
||||
# Allow dev server access from remote IP addresses.
|
||||
# If true, the vite dev server will listen on all addresses, including LAN
|
||||
# and public addresses.
|
||||
VITE_REMOTE_DEV=false
|
||||
|
||||
# The target ComfyUI checkout directory to deploy the frontend code to.
|
||||
# The dist directory will be copied to {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev
|
||||
# Add `--front-end-root {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev`
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -50,6 +50,12 @@ body:
|
||||
description: 'Please copy the output from your browser logs here. You can access this by pressing F12 to toggle the developer tools, then navigating to the Console tab.'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Setting JSON
|
||||
description: 'Please upload the setting file here. The setting file is located at `user/default/comfy.settings.json`'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
|
||||
127
.github/workflows/i18n-custom-nodes.yaml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Update Locales for given custom node repository
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
owner:
|
||||
description: 'Owner of the repository to update locales for'
|
||||
required: true
|
||||
type: string
|
||||
repository:
|
||||
description: 'Repository to update locales for'
|
||||
required: true
|
||||
type: string
|
||||
fork_owner:
|
||||
description: 'Owner of the forked repository'
|
||||
required: false
|
||||
type: string
|
||||
default: 'Comfy-Org'
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'comfyanonymous/ComfyUI'
|
||||
path: 'ComfyUI'
|
||||
ref: master
|
||||
- name: Checkout ComfyUI_frontend
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
||||
path: 'ComfyUI_frontend'
|
||||
- name: Checkout ComfyUI_devtools
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
- name: Checkout custom node repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
||||
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install ComfyUI requirements
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
pip install -r requirements.txt
|
||||
pip install wait-for-it
|
||||
shell: bash
|
||||
working-directory: ComfyUI
|
||||
- name: Install custom node requirements
|
||||
run: |
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
shell: bash
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
- name: Build & Install ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
rm -rf ../ComfyUI/web/*
|
||||
mv dist/* ../ComfyUI/web/
|
||||
shell: bash
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
python main.py --cpu --multi-user &
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
shell: bash
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Capture base i18n
|
||||
run: npx tsx scripts/diff-i18n capture
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
run: npm run locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Diff base vs updated i18n
|
||||
run: npx tsx scripts/diff-i18n diff
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update i18n in custom node repository
|
||||
run: |
|
||||
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
|
||||
install -d "$LOCALE_DIR"
|
||||
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
|
||||
- name: Check and create fork of custom node repository
|
||||
run: |
|
||||
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
|
||||
echo "Fork failed - repository might already be forked"
|
||||
# Exit 0 to prevent the workflow from failing
|
||||
exit 0
|
||||
}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git checkout -b update-locales
|
||||
git add -A
|
||||
git commit -m "Update locales"
|
||||
git remote add org_fork git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git
|
||||
git push org_fork update-locales
|
||||
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
39
.github/workflows/i18n.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Update Locales
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, master, dev* ]
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
# Pipe output so that it doesn't error out on stdout.clearLine
|
||||
run: npm run locale 2>&1 | cat
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Commit updated locales
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git fetch origin ${{ github.head_ref }}
|
||||
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
working-directory: ComfyUI_frontend
|
||||
18
.github/workflows/release.yaml
vendored
@@ -41,3 +41,21 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
make_latest: "true"
|
||||
publish_types:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Release')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm run build:types
|
||||
- name: Publish package
|
||||
run: npm publish --access public
|
||||
working-directory: ./dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
2
.github/workflows/test-browser-exp.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'New Browser Test Expectations'
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
10
.github/workflows/test-ui.yaml
vendored
@@ -15,18 +15,18 @@ jobs:
|
||||
jest-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Run Jest tests
|
||||
run: |
|
||||
npm run test:generate
|
||||
npm run test:generate:examples
|
||||
npm test -- --verbose
|
||||
npm run test:jest:fast -- --verbose
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
playwright-tests-chromium:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
playwright-tests-chromium-2x:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
playwright-tests-mobile-chrome:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
4
.gitignore
vendored
@@ -39,4 +39,6 @@ browser_tests/*/*-win32.png
|
||||
|
||||
.env
|
||||
|
||||
dist.zip
|
||||
dist.zip
|
||||
|
||||
/temp/
|
||||
@@ -1 +1,5 @@
|
||||
npx lint-staged
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
npx.cmd lint-staged
|
||||
else
|
||||
npx lint-staged
|
||||
fi
|
||||
|
||||
17
.i18nrc.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
// This file is intentionally kept in CommonJS format (.cjs)
|
||||
// to resolve compatibility issues with dependencies that require CommonJS.
|
||||
// Do not convert this file to ESModule format unless all dependencies support it.
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
`
|
||||
});
|
||||
@@ -2,5 +2,6 @@
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80
|
||||
}
|
||||
17
CODEOWNERS
Normal file
@@ -0,0 +1,17 @@
|
||||
# Admins
|
||||
* @Comfy-Org/comfy_frontend_devs
|
||||
|
||||
# Maintainers
|
||||
*.md @Comfy-Org/comfy_maintainer
|
||||
/tests-ui/ @Comfy-Org/comfy_maintainer
|
||||
/browser_tests/ @Comfy-Org/comfy_maintainer
|
||||
/.env_example @Comfy-Org/comfy_maintainer
|
||||
|
||||
# Translations (AIGODLIKE team + shinshin86)
|
||||
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
|
||||
# Load 3D extension
|
||||
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
|
||||
|
||||
# Mask Editor extension
|
||||
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs
|
||||
259
README.md
@@ -52,16 +52,38 @@ pause
|
||||
|
||||
### Stable Release
|
||||
|
||||
Stable releases are published weekly in the ComfyUI main repository, aligned with ComfyUI backend's stable release schedule.
|
||||
|
||||
#### Feature Freeze
|
||||
|
||||
There will be a 2-day feature freeze before each stable release. During this period, no new major features will be merged.
|
||||
Stable releases are published bi-weekly in the ComfyUI main repository.
|
||||
|
||||
## Release Summary
|
||||
|
||||
### Major features
|
||||
|
||||
<details>
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
|
||||
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
|
||||
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.4: New mask editor</summary>
|
||||
|
||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.22: Integrated server terminal</summary>
|
||||
|
||||
Press Ctrl + ` to toggle integrated terminal.
|
||||
|
||||
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.7: Keybinding customization</summary>
|
||||
|
||||
@@ -107,6 +129,18 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details>
|
||||
<summary>v1.3.32: **Litegraph** Nested group</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.24: **Litegraph** Group selection</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
|
||||
|
||||
@@ -193,7 +227,96 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
|
||||
</details>
|
||||
|
||||
### Node developers API
|
||||
### Developer APIs
|
||||
|
||||
<details>
|
||||
<summary>v1.3.34: Register about panel badges</summary>
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
aboutPageBadges: [
|
||||
{
|
||||
label: 'Test Badge',
|
||||
url: 'https://example.com',
|
||||
icon: 'pi pi-box'
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.22: Register bottom panel tabs</summary>
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension',
|
||||
bottomPanelTabs: [
|
||||
{
|
||||
id: 'TestTab',
|
||||
title: 'Test Tab',
|
||||
type: 'custom',
|
||||
render: (el) => {
|
||||
el.innerHTML = '<div>Custom tab</div>'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.22: New settings API</summary>
|
||||
|
||||
Legacy settings API.
|
||||
|
||||
```js
|
||||
// Register a new setting
|
||||
app.ui.settings.addSetting({
|
||||
id: 'TestSetting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'Hello, world!'
|
||||
})
|
||||
|
||||
// Get the value of a setting
|
||||
const value = app.ui.settings.getSettingValue('TestSetting')
|
||||
|
||||
// Set the value of a setting
|
||||
app.ui.settings.setSettingValue('TestSetting', 'Hello, universe!')
|
||||
```
|
||||
|
||||
New settings API.
|
||||
|
||||
```js
|
||||
// Register a new setting
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
settings: [
|
||||
{
|
||||
id: 'TestSetting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'Hello, world!'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Get the value of a setting
|
||||
const value = app.extensionManager.setting.get('TestSetting')
|
||||
|
||||
// Set the value of a setting
|
||||
app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.7: Register commands and keybindings</summary>
|
||||
@@ -298,6 +421,15 @@ We will support custom icons later.
|
||||
|
||||
## Development
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Pinia](https://pinia.vuejs.org/) for state management
|
||||
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
|
||||
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
|
||||
- [zod](https://zod.dev/) for schema validation
|
||||
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit
|
||||
@@ -308,11 +440,33 @@ hook is used to auto-format code on commit.
|
||||
Note: The dev server will NOT load any extension from the ComfyUI server. Only
|
||||
core extensions will be loaded.
|
||||
|
||||
- Run `npm install` to install the necessary packages
|
||||
- Start local ComfyUI backend at `localhost:8188`
|
||||
- Run `npm run dev` to start the dev server
|
||||
- Run `npm run dev:electron` to start the dev server with electron API mocked
|
||||
|
||||
### Test
|
||||
#### Access dev server on touch devices
|
||||
|
||||
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.
|
||||
|
||||
After you start the dev server, you should see following logs:
|
||||
|
||||
```
|
||||
> comfyui-frontend@1.3.42 dev
|
||||
> vite
|
||||
|
||||
|
||||
VITE v5.4.6 ready in 488 ms
|
||||
|
||||
➜ Local: http://localhost:5173/
|
||||
➜ Network: http://172.21.80.1:5173/
|
||||
➜ Network: http://192.168.2.20:5173/
|
||||
➜ press h + enter to show help
|
||||
```
|
||||
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
### Unit Test
|
||||
|
||||
- `git clone https://github.com/comfyanonymous/ComfyUI_examples.git` to `tests-ui/ComfyUI_examples` or the EXAMPLE_REPO_PATH location specified in .env
|
||||
- `npm i` to install all dependencies
|
||||
@@ -320,6 +474,16 @@ core extensions will be loaded.
|
||||
- `npm run test:generate:examples` to extract the example workflows
|
||||
- `npm run test` to execute all unit tests.
|
||||
|
||||
### Component Test
|
||||
|
||||
Component test verifies Vue components in `src/components/`.
|
||||
|
||||
- `npm run test:component` to execute all component tests.
|
||||
|
||||
### Playwright Test
|
||||
|
||||
Playwright test verifies the whole app. See <https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/README.md> for details.
|
||||
|
||||
### LiteGraph
|
||||
|
||||
This repo is using litegraph package hosted on <https://github.com/Comfy-Org/litegraph.js>. Any changes to litegraph should be submitted in that repo instead.
|
||||
@@ -327,11 +491,88 @@ This repo is using litegraph package hosted on <https://github.com/Comfy-Org/lit
|
||||
### Test litegraph changes
|
||||
|
||||
- Run `npm link` in the local litegraph repo.
|
||||
- Run `npm uninstall @comfyorg/litegraph` in this repo.
|
||||
- Run `npm link @comfyorg/litegraph` in this repo.
|
||||
|
||||
This will replace the litegraph package in this repo with the local litegraph repo.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
- en (English)
|
||||
- zh (中文)
|
||||
- ru (Русский)
|
||||
- ja (日本語)
|
||||
- ko (한국어)
|
||||
|
||||
### How to Add a New Language
|
||||
|
||||
We welcome the addition of new languages. You can add a new language by following these steps:
|
||||
|
||||
#### 1. Generate language files
|
||||
We use [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/blob/master/packages/lobe-i18n/README.md) as our translation tool, which integrates with LLM for efficient localization.
|
||||
|
||||
Update the configuration file to include the new language(s) you wish to add:
|
||||
|
||||
|
||||
```javascript
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
entry: 'src/locales/en.json', // Base language file
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja'], // Add the new language(s) here
|
||||
});
|
||||
```
|
||||
|
||||
Set your OpenAI API Key by running the following command:
|
||||
|
||||
```sh
|
||||
npx lobe-i18n --option
|
||||
```
|
||||
|
||||
Once configured, generate the translation files with:
|
||||
|
||||
```sh
|
||||
npx lobe-i18n locale
|
||||
```
|
||||
|
||||
This will create the language files for the specified languages in the configuration.
|
||||
|
||||
#### 2. Update i18n Configuration
|
||||
|
||||
Import the newly generated locale file(s) in the `src/i18n.ts` file to include them in the application's i18n setup.
|
||||
|
||||
#### 3. Enable Selection of the New Language
|
||||
|
||||
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
// Add the new language(s) here
|
||||
options: [
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'zh', text: '中文' },
|
||||
{ value: 'ru', text: 'Русский' },
|
||||
{ value: 'ja', text: '日本語' }
|
||||
],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
```
|
||||
|
||||
This will make the new language selectable in the application's settings.
|
||||
|
||||
#### 4. Test the Translations
|
||||
|
||||
Start the development server, switch to the new language, and verify the translations.
|
||||
You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
|
||||
|
||||
## Deploy
|
||||
|
||||
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
|
||||
|
||||
@@ -34,6 +34,8 @@ There are two ways to run the tests:
|
||||
```
|
||||
This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
|
||||
|
||||
To run the same test multiple times in Playwright's UI mode, you must launch the main ComfyUI process with the `--multi-user` flag.
|
||||
|
||||

|
||||
|
||||
## Screenshot Expectations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Response } from '@playwright/test'
|
||||
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
import { comfyPageFixture } from './ComfyPage'
|
||||
import { comfyPageFixture } from './fixtures/ComfyPage'
|
||||
import { webSocketFixture } from './fixtures/ws.ts'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
@@ -11,10 +11,6 @@ test.describe('Actionbar', () => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
/**
|
||||
* This test ensures that the autoqueue change mode can only queue one change at a time
|
||||
*/
|
||||
@@ -56,7 +52,9 @@ test.describe('Actionbar', () => {
|
||||
(n) => n.type === 'EmptyLatentImage'
|
||||
)
|
||||
node.widgets[0].value = value
|
||||
window['app'].workflowManager.activeWorkflow.changeTracker.checkState()
|
||||
window[
|
||||
'app'
|
||||
].extensionManager.workflow.activeWorkflow.changeTracker.checkState()
|
||||
}, value)
|
||||
}
|
||||
|
||||
|
||||
227
browser_tests/assets/mixed_graph_items.json
Normal file
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 420,
|
||||
"1": 130
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 820,
|
||||
"1": 130
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 30,
|
||||
"1": 130
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 0,
|
||||
"title": "Group",
|
||||
"bounding": [
|
||||
406.9701232910156,
|
||||
59.079444885253906,
|
||||
335,
|
||||
345.6000061035156
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Group Parent",
|
||||
"bounding": [
|
||||
796.9703979492188,
|
||||
14.796443939208984,
|
||||
355,
|
||||
399.20001220703125
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Group Child",
|
||||
"bounding": [
|
||||
806.9703979492188,
|
||||
58.39643096923828,
|
||||
335,
|
||||
345.6000061035156
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
128
browser_tests/assets/old_workflow_converted_input.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ControlNetApplyAdvanced",
|
||||
"pos": {
|
||||
"0": 449,
|
||||
"1": 204
|
||||
},
|
||||
"size": [
|
||||
340.20001220703125,
|
||||
166
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "control_net",
|
||||
"type": "CONTROL_NET",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "strength",
|
||||
"type": "FLOAT",
|
||||
"link": 1,
|
||||
"widget": {
|
||||
"name": "strength"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetApplyAdvanced"
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": {
|
||||
"0": 177,
|
||||
"1": 265
|
||||
},
|
||||
"size": [
|
||||
210,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"widget": {
|
||||
"name": "strength"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
"fixed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
4,
|
||||
"FLOAT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 47.541666666666515,
|
||||
"1": 186.9375
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
90
browser_tests/assets/oversized_group.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
37,
|
||||
98
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Group",
|
||||
"bounding": [
|
||||
23,
|
||||
23,
|
||||
900,
|
||||
825
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
49
browser_tests/assets/simple_slider.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "DevToolsSimpleSlider",
|
||||
"pos": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"label": "FLOAT"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsSimpleSlider"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.5
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
116
browser_tests/assets/single_connected_reroute_node.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"last_node_id": 16,
|
||||
"last_link_id": 18,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [620, 260],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "Reroute",
|
||||
"pos": [510, 280],
|
||||
"size": [75, 26],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [18],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [160, 240],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [13],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[13, 4, 2, 13, 0, "*"],
|
||||
[18, 13, 0, 12, 1, "VAE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
42
browser_tests/assets/string_input.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DevToolsNodeWithStringInput",
|
||||
"pos": [
|
||||
15,
|
||||
48
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithStringInput"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Browser tab title', () => {
|
||||
test.describe('Beta Menu', () => {
|
||||
@@ -7,30 +7,24 @@ test.describe('Browser tab title', () => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('Can display workflow name', async ({ comfyPage }) => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].workflowManager.activeWorkflow.name
|
||||
return window['app'].extensionManager.workflow.activeWorkflow.filename
|
||||
})
|
||||
// Note: unsaved workflow name is always prepended with "*".
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
})
|
||||
|
||||
// Broken by https://github.com/Comfy-Org/ComfyUI_frontend/pull/893
|
||||
// Release blocker for v1.3.0
|
||||
// Failing on CI
|
||||
// Cannot reproduce locally
|
||||
test.skip('Can display workflow name with unsaved changes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].workflowManager.activeWorkflow.name
|
||||
return window['app'].extensionManager.workflow.activeWorkflow.filename
|
||||
})
|
||||
// Note: unsaved workflow name is always prepended with "*".
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
|
||||
|
||||
await comfyPage.menu.saveWorkflow('test')
|
||||
await comfyPage.menu.topbar.saveWorkflow('test')
|
||||
expect(await comfyPage.page.title()).toBe('test - ComfyUI')
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
@@ -40,7 +34,7 @@ test.describe('Browser tab title', () => {
|
||||
|
||||
// Delete the saved workflow for cleanup.
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
window['app'].workflowManager.activeWorkflow.delete()
|
||||
return window['app'].extensionManager.workflow.activeWorkflow.delete()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './ComfyPage'
|
||||
} from './fixtures/ComfyPage'
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
@@ -16,6 +16,51 @@ async function afterChange(comfyPage: ComfyPage) {
|
||||
}
|
||||
|
||||
test.describe('Change Tracker', () => {
|
||||
test.describe('Undo/Redo', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Can undo multiple operations', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
// Save, confirm no errors & workflow modified flag removed
|
||||
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await expect(node).toBeCollapsed()
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(2)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(1)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
test('Can group multiple change actions into a single transaction', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -26,6 +71,7 @@ test.describe('Change Tracker', () => {
|
||||
|
||||
// Make changes outside set
|
||||
// Bypass + collapse node
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeCollapsed()
|
||||
@@ -39,8 +85,12 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).not.toBeBypassed()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
|
||||
// Prevent clicks registering a double-click
|
||||
await comfyPage.clickEmptySpace()
|
||||
await node.click('title')
|
||||
|
||||
// Run again, but within a change transaction
|
||||
beforeChange(comfyPage)
|
||||
await beforeChange(comfyPage)
|
||||
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
@@ -48,7 +98,7 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).toBeBypassed()
|
||||
|
||||
// End transaction
|
||||
afterChange(comfyPage)
|
||||
await afterChange(comfyPage)
|
||||
|
||||
// Ensure undo reverts both changes
|
||||
await comfyPage.ctrlZ()
|
||||
@@ -56,7 +106,7 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).not.toBeCollapsed()
|
||||
})
|
||||
|
||||
test('Can group multiple transaction calls into a single one', async ({
|
||||
test('Can nest multiple change transactions without adding undo steps', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
@@ -80,6 +130,7 @@ test.describe('Change Tracker', () => {
|
||||
const multipleChanges = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
// Call other actions that uses begin/endChange
|
||||
await node.click('title')
|
||||
await collapse()
|
||||
await bypassAndPin()
|
||||
await afterChange(comfyPage)
|
||||
@@ -97,4 +148,20 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).toBePinned()
|
||||
await expect(node).toBeCollapsed()
|
||||
})
|
||||
|
||||
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].graph.extra.foo = 'bar'
|
||||
})
|
||||
// Click empty space to trigger a change detection.
|
||||
await comfyPage.clickEmptySpace()
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
})
|
||||
|
||||
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.pan({ x: 10, y: 10 })
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
const customColorPalettes = {
|
||||
obsidian: {
|
||||
@@ -135,12 +135,6 @@ test.describe('Color Palette', () => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', {})
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
})
|
||||
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
@@ -158,11 +152,6 @@ test.describe('Node Color Adjustments', () => {
|
||||
await comfyPage.loadWorkflow('every_node_color')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
})
|
||||
|
||||
test('should adjust opacity via node opacity setting', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -222,7 +211,7 @@ test.describe('Node Color Adjustments', () => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
|
||||
const node = await comfyPage.getFirstNodeRef()
|
||||
await node.clickContextMenuOption('Colors')
|
||||
await node?.clickContextMenuOption('Colors')
|
||||
})
|
||||
|
||||
test('should persist color adjustments when changing custom node colors', async ({
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Should execute command', async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Copy Paste', () => {
|
||||
test('Can copy and paste node', async ({ comfyPage }) => {
|
||||
@@ -15,23 +15,15 @@ test.describe('Copy Paste', () => {
|
||||
await textBox.click()
|
||||
const originalString = await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlC(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
const resultString = await textBox.inputValue()
|
||||
expect(resultString).toBe(originalString + originalString)
|
||||
})
|
||||
|
||||
test('Can copy and paste widget value', async ({ comfyPage }) => {
|
||||
// Copy width value (512) from empty latent node to KSampler's seed.
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 718,
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC()
|
||||
// KSampler's seed
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
@@ -39,7 +31,15 @@ test.describe('Copy Paste', () => {
|
||||
y: 281
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlC(null)
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 718,
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
|
||||
})
|
||||
@@ -51,14 +51,14 @@ test.describe('Copy Paste', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlC(null)
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlC(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'paste-in-text-area-with-node-previously-copied.png'
|
||||
)
|
||||
@@ -69,10 +69,10 @@ test.describe('Copy Paste', () => {
|
||||
await textBox.click()
|
||||
await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlC(null)
|
||||
// Unfocus textbox.
|
||||
await comfyPage.page.mouse.click(10, 10)
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV(null)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Load workflow warning', () => {
|
||||
test('Should display a warning when loading a workflow with missing nodes', async ({
|
||||
@@ -36,6 +36,7 @@ test.describe('Execution error', () => {
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for the element with the .comfy-execution-error selector to be visible
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
@@ -51,7 +52,9 @@ test.describe('Missing models warning', () => {
|
||||
}, comfyPage.url)
|
||||
})
|
||||
|
||||
test('Should display a warning when missing models are found', async ({
|
||||
// Flaky test after parallelization
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
|
||||
test.skip('Should display a warning when missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// The fake_model.safetensors is served by
|
||||
@@ -63,49 +66,23 @@ test.describe('Missing models warning', () => {
|
||||
|
||||
const downloadButton = comfyPage.page.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
const downloadPromise = comfyPage.page.waitForEvent('download')
|
||||
await downloadButton.click()
|
||||
|
||||
const downloadComplete = comfyPage.page.locator('.download-complete')
|
||||
await expect(downloadComplete).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can configure download folder', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
|
||||
const folderSelectToggle = comfyPage.page.locator(
|
||||
'.model-path-select-checkbox'
|
||||
)
|
||||
const folderSelect = comfyPage.page.locator('.model-path-select')
|
||||
await expect(folderSelectToggle).toBeVisible()
|
||||
await expect(folderSelect).not.toBeVisible()
|
||||
|
||||
await folderSelectToggle.click() // show the selectors
|
||||
await expect(folderSelect).toBeVisible()
|
||||
|
||||
await folderSelect.click() // open dropdown
|
||||
await expect(folderSelect).toHaveClass(/p-select-open/)
|
||||
|
||||
await folderSelect.click() // close the dropdown
|
||||
await expect(folderSelect).not.toHaveClass(/p-select-open/)
|
||||
|
||||
await folderSelectToggle.click() // hide the selectors
|
||||
await expect(folderSelect).not.toBeVisible()
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
// Restore default setting value
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
||||
})
|
||||
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const searchBox = comfyPage.page.locator('.settings-content')
|
||||
await expect(searchBox).toBeVisible()
|
||||
const settingsContent = comfyPage.page.locator('.settings-content')
|
||||
await expect(settingsContent).toBeVisible()
|
||||
const isUsableHeight = await settingsContent.evaluate(
|
||||
(el) => el.clientHeight > 30
|
||||
)
|
||||
expect(isUsableHeight).toBeTruthy()
|
||||
})
|
||||
|
||||
test('Can open settings with hotkey', async ({ comfyPage }) => {
|
||||
@@ -121,7 +98,7 @@ test.describe('Settings', () => {
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const maxSpeed = 2.5
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
test.step('Setting should persist', async () => {
|
||||
await test.step('Setting should persist', async () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { expect, Locator } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('Should allow registering topbar commands', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
@@ -52,7 +48,7 @@ test.describe('Topbar commands', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const menuItem: Locator = await comfyPage.menu.topbar.getMenuItem('ext')
|
||||
const menuItem = comfyPage.menu.topbar.getMenuItem('ext')
|
||||
expect(await menuItem.count()).toBe(0)
|
||||
})
|
||||
|
||||
@@ -83,4 +79,82 @@ test.describe('Topbar commands', () => {
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test('Should allow adding settings', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
name: 'TestExtension1',
|
||||
settings: [
|
||||
{
|
||||
id: 'TestSetting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'Hello, world!',
|
||||
onChange: () => {
|
||||
window['changeCount'] = (window['changeCount'] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
// onChange is called when the setting is first added
|
||||
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
|
||||
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!')
|
||||
|
||||
await comfyPage.setSetting('TestSetting', 'Hello, universe!')
|
||||
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!')
|
||||
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
|
||||
})
|
||||
|
||||
test('Should allow setting boolean settings', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
name: 'TestExtension1',
|
||||
settings: [
|
||||
{
|
||||
id: 'Comfy.TestSetting',
|
||||
name: 'Test Setting',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
window['changeCount'] = (window['changeCount'] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false)
|
||||
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
|
||||
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting')
|
||||
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true)
|
||||
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('About panel', () => {
|
||||
test('Should allow adding badges', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
name: 'TestExtension1',
|
||||
aboutPageBadges: [
|
||||
{
|
||||
label: 'Test Badge',
|
||||
url: 'https://example.com',
|
||||
icon: 'pi pi-box'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.settingDialog.goToAboutPanel()
|
||||
const badge = comfyPage.page.locator('.about-badge').last()
|
||||
expect(badge).toBeDefined()
|
||||
expect(await badge.textContent()).toContain('Test Badge')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
41
browser_tests/fixtures/UserSelectPage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Page } from 'playwright'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
public readonly url: string,
|
||||
public readonly page: Page
|
||||
) {}
|
||||
|
||||
get selectionUrl() {
|
||||
return this.url + '/user-select'
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this.page.locator('#comfy-user-selection')
|
||||
}
|
||||
|
||||
get newUserInput() {
|
||||
return this.container.locator('#new-user-input')
|
||||
}
|
||||
|
||||
get existingUserSelect() {
|
||||
return this.container.locator('#existing-user-select')
|
||||
}
|
||||
|
||||
get nextButton() {
|
||||
return this.container.getByText('Next')
|
||||
}
|
||||
}
|
||||
|
||||
export const userSelectPageFixture = base.extend<{
|
||||
userSelectPage: UserSelectPage
|
||||
}>({
|
||||
userSelectPage: async ({ page }, use) => {
|
||||
const userSelectPage = new UserSelectPage(
|
||||
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
|
||||
page
|
||||
)
|
||||
await use(userSelectPage)
|
||||
}
|
||||
})
|
||||
79
browser_tests/fixtures/components/ComfyNodeSearchBox.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ComfyNodeSearchFilterSelectionPanel {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
async selectFilterType(filterType: string) {
|
||||
await this.page
|
||||
.locator(
|
||||
`.filter-type-select .p-togglebutton-label:has-text("${filterType}")`
|
||||
)
|
||||
.click()
|
||||
}
|
||||
|
||||
async selectFilterValue(filterValue: string) {
|
||||
await this.page.locator('.filter-value-select .p-select-dropdown').click()
|
||||
await this.page
|
||||
.locator(
|
||||
`.p-select-overlay .p-select-list .p-select-option-label:text-is("${filterValue}")`
|
||||
)
|
||||
.click()
|
||||
}
|
||||
|
||||
async addFilter(filterValue: string, filterType: string) {
|
||||
await this.selectFilterType(filterType)
|
||||
await this.selectFilterValue(filterValue)
|
||||
await this.page.locator('.p-button-label:has-text("Add")').click()
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyNodeSearchBox {
|
||||
public readonly input: Locator
|
||||
public readonly dropdown: Locator
|
||||
public readonly filterSelectionPanel: ComfyNodeSearchFilterSelectionPanel
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.input = page.locator(
|
||||
'.comfy-vue-node-search-container input[type="text"]'
|
||||
)
|
||||
this.dropdown = page.locator(
|
||||
'.comfy-vue-node-search-container .p-autocomplete-list'
|
||||
)
|
||||
this.filterSelectionPanel = new ComfyNodeSearchFilterSelectionPanel(page)
|
||||
}
|
||||
|
||||
get filterButton() {
|
||||
return this.page.locator('.comfy-vue-node-search-container .filter-button')
|
||||
}
|
||||
|
||||
async fillAndSelectFirstNode(
|
||||
nodeName: string,
|
||||
options?: { suggestionIndex: number }
|
||||
) {
|
||||
await this.input.waitFor({ state: 'visible' })
|
||||
await this.input.fill(nodeName)
|
||||
await this.dropdown.waitFor({ state: 'visible' })
|
||||
// Wait for some time for the auto complete list to update.
|
||||
// The auto complete list is debounced and may take some time to update.
|
||||
await this.page.waitForTimeout(500)
|
||||
await this.dropdown
|
||||
.locator('li')
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
.click()
|
||||
}
|
||||
|
||||
async addFilter(filterValue: string, filterType: string) {
|
||||
await this.filterButton.click()
|
||||
await this.filterSelectionPanel.addFilter(filterValue, filterType)
|
||||
}
|
||||
|
||||
get filterChips() {
|
||||
return this.page.locator(
|
||||
'.comfy-vue-node-search-container .p-autocomplete-chip-item'
|
||||
)
|
||||
}
|
||||
|
||||
async removeFilter(index: number) {
|
||||
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
|
||||
}
|
||||
}
|
||||
40
browser_tests/fixtures/components/SettingDialog.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export class SettingDialog {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
async open() {
|
||||
const button = this.page.locator('button.comfy-settings-btn:visible')
|
||||
await button.click()
|
||||
await this.page.waitForSelector('div.settings-container')
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a text setting
|
||||
* @param id - The id of the setting
|
||||
* @param value - The value to set
|
||||
*/
|
||||
async setStringSetting(id: string, value: string) {
|
||||
const settingInputDiv = this.page.locator(
|
||||
`div.settings-container div[id="${id}"]`
|
||||
)
|
||||
await settingInputDiv.locator('input').fill(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the value of a boolean setting
|
||||
* @param id - The id of the setting
|
||||
*/
|
||||
async toggleBooleanSetting(id: string) {
|
||||
const settingInputDiv = this.page.locator(
|
||||
`div.settings-container div[id="${id}"]`
|
||||
)
|
||||
await settingInputDiv.locator('input').click()
|
||||
}
|
||||
|
||||
async goToAboutPanel() {
|
||||
const aboutButton = this.page.locator('li[aria-label="About"]')
|
||||
await aboutButton.click()
|
||||
await this.page.waitForSelector('div.about-container')
|
||||
}
|
||||
}
|
||||
149
browser_tests/fixtures/components/SidebarTab.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
class SidebarTab {
|
||||
constructor(
|
||||
public readonly page: Page,
|
||||
public readonly tabId: string
|
||||
) {}
|
||||
|
||||
get tabButton() {
|
||||
return this.page.locator(`.${this.tabId}-tab-button`)
|
||||
}
|
||||
|
||||
get selectedTabButton() {
|
||||
return this.page.locator(
|
||||
`.${this.tabId}-tab-button.side-bar-button-selected`
|
||||
)
|
||||
}
|
||||
|
||||
async open() {
|
||||
if (await this.selectedTabButton.isVisible()) {
|
||||
return
|
||||
}
|
||||
await this.tabButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeLibrarySidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'node-library')
|
||||
}
|
||||
|
||||
get nodeLibrarySearchBoxInput() {
|
||||
return this.page.locator('.node-lib-search-box input[type="text"]')
|
||||
}
|
||||
|
||||
get nodeLibraryTree() {
|
||||
return this.page.locator('.node-lib-tree-explorer')
|
||||
}
|
||||
|
||||
get nodePreview() {
|
||||
return this.page.locator('.node-lib-node-preview')
|
||||
}
|
||||
|
||||
get tabContainer() {
|
||||
return this.page.locator('.sidebar-content-container')
|
||||
}
|
||||
|
||||
get newFolderButton() {
|
||||
return this.tabContainer.locator('.new-folder-button')
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open()
|
||||
await this.nodeLibraryTree.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.tabButton.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.tabButton.click()
|
||||
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
folderSelector(folderName: string) {
|
||||
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
|
||||
}
|
||||
|
||||
getFolder(folderName: string) {
|
||||
return this.page.locator(this.folderSelector(folderName))
|
||||
}
|
||||
|
||||
nodeSelector(nodeName: string) {
|
||||
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
|
||||
}
|
||||
|
||||
getNode(nodeName: string) {
|
||||
return this.page.locator(this.nodeSelector(nodeName))
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowsSidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'workflows')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.workflows-sidebar-tab')
|
||||
}
|
||||
|
||||
get browseGalleryButton() {
|
||||
return this.root.locator('.browse-templates-button')
|
||||
}
|
||||
|
||||
get newBlankWorkflowButton() {
|
||||
return this.root.locator('.new-blank-workflow-button')
|
||||
}
|
||||
|
||||
get openWorkflowButton() {
|
||||
return this.root.locator('.open-workflow-button')
|
||||
}
|
||||
|
||||
async getOpenedWorkflowNames() {
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-open .node-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveWorkflowName() {
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async getTopLevelSavedWorkflowNames() {
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-browse .node-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async switchToWorkflow(workflowName: string) {
|
||||
const workflowLocator = this.getOpenedItem(workflowName)
|
||||
await workflowLocator.click()
|
||||
await this.page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
getOpenedItem(name: string) {
|
||||
return this.root.locator('.comfyui-workflows-open .node-label', {
|
||||
hasText: name
|
||||
})
|
||||
}
|
||||
|
||||
getPersistedItem(name: string) {
|
||||
return this.root.locator('.comfyui-workflows-browse .node-label', {
|
||||
hasText: name
|
||||
})
|
||||
}
|
||||
|
||||
async renameWorkflow(locator: Locator, newName: string) {
|
||||
await locator.click({ button: 'right' })
|
||||
await this.page
|
||||
.locator('.p-contextmenu-item-content', { hasText: 'Rename' })
|
||||
.click()
|
||||
await this.page.keyboard.type(newName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
await this.page.waitForTimeout(300)
|
||||
}
|
||||
}
|
||||
92
browser_tests/fixtures/components/Topbar.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class Topbar {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
async getTabNames(): Promise<string[]> {
|
||||
return await this.page
|
||||
.locator('.workflow-tabs .workflow-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async openSubmenuMobile() {
|
||||
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
||||
}
|
||||
|
||||
getMenuItem(itemLabel: string): Locator {
|
||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||
}
|
||||
|
||||
getWorkflowTab(tabName: string): Locator {
|
||||
return this.page
|
||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||
.locator('..')
|
||||
}
|
||||
|
||||
async closeWorkflowTab(tabName: string) {
|
||||
const tab = this.getWorkflowTab(tabName)
|
||||
await tab.locator('.close-button').click({ force: true })
|
||||
}
|
||||
|
||||
getSaveDialog(): Locator {
|
||||
return this.page.locator('.p-dialog-content input')
|
||||
}
|
||||
|
||||
saveWorkflow(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Save')
|
||||
}
|
||||
|
||||
saveWorkflowAs(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Save As')
|
||||
}
|
||||
|
||||
exportWorkflow(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Export')
|
||||
}
|
||||
|
||||
async _saveWorkflow(
|
||||
workflowName: string,
|
||||
command: 'Save' | 'Save As' | 'Export'
|
||||
) {
|
||||
await this.triggerTopbarCommand(['Workflow', command])
|
||||
await this.getSaveDialog().fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
|
||||
// Wait for workflow service to finish saving
|
||||
await this.page.waitForFunction(
|
||||
() => !window['app'].extensionManager.workflow.isBusy,
|
||||
undefined,
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
// Wait for the dialog to close.
|
||||
await this.getSaveDialog().waitFor({ state: 'hidden', timeout: 500 })
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
if (path.length < 2) {
|
||||
throw new Error('Path is too short')
|
||||
}
|
||||
|
||||
const tabName = path[0]
|
||||
const topLevelMenu = this.page.locator(
|
||||
`.top-menubar .p-menubar-item-label:text-is("${tabName}")`
|
||||
)
|
||||
await topLevelMenu.waitFor({ state: 'visible' })
|
||||
await topLevelMenu.click()
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const commandName = path[i]
|
||||
const menuItem = this.page
|
||||
.locator(
|
||||
`.top-menubar .p-menubar-submenu .p-menubar-item:has-text("${commandName}")`
|
||||
)
|
||||
.first()
|
||||
await menuItem.waitFor({ state: 'visible' })
|
||||
await menuItem.hover()
|
||||
|
||||
if (i === path.length - 1) {
|
||||
await menuItem.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
browser_tests/fixtures/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
257
browser_tests/fixtures/utils/litegraphUtils.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { ManageGroupNode } from '../../helpers/manageGroupNode'
|
||||
import type { NodeId } from '../../../src/types/comfyWorkflow'
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
import type { Position, Size } from '../types'
|
||||
|
||||
export class NodeSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
readonly index: number,
|
||||
readonly node: NodeReference
|
||||
) {}
|
||||
async getPosition() {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(
|
||||
node.getConnectionPos(type === 'input', index)
|
||||
)
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
async getLinkCount() {
|
||||
return await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
return node.inputs[index].link == null ? 0 : 1
|
||||
}
|
||||
return node.outputs[index].links?.length ?? 0
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
}
|
||||
async removeLinks() {
|
||||
await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
node.disconnectInput(index)
|
||||
} else {
|
||||
node.disconnectOutput(index)
|
||||
}
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeWidgetReference {
|
||||
constructor(
|
||||
readonly index: number,
|
||||
readonly node: NodeReference
|
||||
) {}
|
||||
|
||||
async getPosition(): Promise<Position> {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
const widget = node.widgets[index]
|
||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||
|
||||
const [x, y, w, h] = node.getBounding()
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
x + w / 2,
|
||||
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
||||
])
|
||||
},
|
||||
[this.node.id, this.index] as const
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeReference {
|
||||
constructor(
|
||||
readonly id: NodeId,
|
||||
readonly comfyPage: ComfyPage
|
||||
) {}
|
||||
async exists(): Promise<boolean> {
|
||||
return await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return !!node
|
||||
}, this.id)
|
||||
}
|
||||
getType(): Promise<string> {
|
||||
return this.getProperty('type')
|
||||
}
|
||||
async getPosition(): Promise<Position> {
|
||||
const pos = await this.comfyPage.convertOffsetToCanvas(
|
||||
await this.getProperty<[number, number]>('pos')
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
async getBounding(): Promise<Position & Size> {
|
||||
const [x, y, width, height]: [number, number, number, number] =
|
||||
await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error('Node not found')
|
||||
return node.getBounding()
|
||||
}, this.id)
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
async getSize(): Promise<Size> {
|
||||
const size = await this.getProperty<[number, number]>('size')
|
||||
return {
|
||||
width: size[0],
|
||||
height: size[1]
|
||||
}
|
||||
}
|
||||
async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> {
|
||||
return await this.getProperty('flags')
|
||||
}
|
||||
async isPinned() {
|
||||
return !!(await this.getFlags()).pinned
|
||||
}
|
||||
async isCollapsed() {
|
||||
return !!(await this.getFlags()).collapsed
|
||||
}
|
||||
async isBypassed() {
|
||||
return (await this.getProperty<number | null | undefined>('mode')) === 4
|
||||
}
|
||||
async getProperty<T>(prop: string): Promise<T> {
|
||||
return await this.comfyPage.page.evaluate(
|
||||
([id, prop]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error('Node not found')
|
||||
return node[prop]
|
||||
},
|
||||
[this.id, prop] as const
|
||||
)
|
||||
}
|
||||
async getOutput(index: number) {
|
||||
return new NodeSlotReference('output', index, this)
|
||||
}
|
||||
async getInput(index: number) {
|
||||
return new NodeSlotReference('input', index, this)
|
||||
}
|
||||
async getWidget(index: number) {
|
||||
return new NodeWidgetReference(index, this)
|
||||
}
|
||||
async click(
|
||||
position: 'title' | 'collapse',
|
||||
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
|
||||
) {
|
||||
const nodePos = await this.getPosition()
|
||||
const nodeSize = await this.getSize()
|
||||
let clickPos: Position
|
||||
switch (position) {
|
||||
case 'title':
|
||||
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 }
|
||||
break
|
||||
case 'collapse':
|
||||
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
|
||||
break
|
||||
default:
|
||||
throw new Error(`Invalid click position ${position}`)
|
||||
}
|
||||
|
||||
const moveMouseToEmptyArea = options?.moveMouseToEmptyArea
|
||||
if (options) {
|
||||
delete options.moveMouseToEmptyArea
|
||||
}
|
||||
|
||||
await this.comfyPage.canvas.click({
|
||||
...options,
|
||||
position: clickPos
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
if (moveMouseToEmptyArea) {
|
||||
await this.comfyPage.moveMouseToEmptyArea()
|
||||
}
|
||||
}
|
||||
async copy() {
|
||||
await this.click('title')
|
||||
await this.comfyPage.ctrlC()
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
async connectWidget(
|
||||
originSlotIndex: number,
|
||||
targetNode: NodeReference,
|
||||
targetWidgetIndex: number
|
||||
) {
|
||||
const originSlot = await this.getOutput(originSlotIndex)
|
||||
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
||||
await this.comfyPage.dragAndDrop(
|
||||
await originSlot.getPosition(),
|
||||
await targetWidget.getPosition()
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
async connectOutput(
|
||||
originSlotIndex: number,
|
||||
targetNode: NodeReference,
|
||||
targetSlotIndex: number
|
||||
) {
|
||||
const originSlot = await this.getOutput(originSlotIndex)
|
||||
const targetSlot = await targetNode.getInput(targetSlotIndex)
|
||||
await this.comfyPage.dragAndDrop(
|
||||
await originSlot.getPosition(),
|
||||
await targetSlot.getPosition()
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
async getContextMenuOptionNames() {
|
||||
await this.click('title', { button: 'right' })
|
||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||
return await ctx.locator('.litemenu-entry').allInnerTexts()
|
||||
}
|
||||
async clickContextMenuOption(optionText: string) {
|
||||
await this.click('title', { button: 'right' })
|
||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||
await ctx.getByText(optionText).click()
|
||||
}
|
||||
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
|
||||
await this.clickContextMenuOption('Convert to Group Node')
|
||||
await this.comfyPage.promptDialogInput.fill(groupNodeName)
|
||||
await this.comfyPage.page.keyboard.press('Enter')
|
||||
await this.comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await this.comfyPage.nextFrame()
|
||||
const nodes = await this.comfyPage.getNodeRefsByType(
|
||||
`workflow>${groupNodeName}`
|
||||
)
|
||||
if (nodes.length !== 1) {
|
||||
throw new Error(`Did not find single group node (found=${nodes.length})`)
|
||||
}
|
||||
return nodes[0]
|
||||
}
|
||||
async manageGroupNode() {
|
||||
await this.clickContextMenuOption('Manage Group Node')
|
||||
await this.comfyPage.nextFrame()
|
||||
return new ManageGroupNode(
|
||||
this.comfyPage.page,
|
||||
this.comfyPage.page.locator('.comfy-group-manage')
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Graph Canvas Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,11 +1,8 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { ComfyPage, NodeReference, comfyPageFixture as test } from './ComfyPage'
|
||||
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
import type { NodeReference } from './fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Node library sidebar', () => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
const groupNodeCategory = 'group nodes>workflow'
|
||||
@@ -19,11 +16,6 @@ test.describe('Group Node', () => {
|
||||
await libraryTab.open()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
|
||||
await libraryTab.close()
|
||||
})
|
||||
|
||||
test('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
|
||||
})
|
||||
@@ -85,8 +77,13 @@ test.describe('Group Node', () => {
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
test('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
// The 500ms fixed delay on the search results is causing flakiness
|
||||
// Potential solution: add a spinner state when the search is in progress,
|
||||
// and observe that state from the test. Blocker: the PrimeVue AutoComplete
|
||||
// does not have a v-model on the query, so we cannot observe the raw
|
||||
// query update, and thus cannot set the spinning state between the raw query
|
||||
// update and the debounced search update.
|
||||
test.skip('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
@@ -98,6 +95,7 @@ test.describe('Group Node', () => {
|
||||
})
|
||||
|
||||
test('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
||||
await comfyPage.page.mouse.move(47, 173)
|
||||
const tooltipTimeout = 500
|
||||
@@ -105,6 +103,36 @@ test.describe('Group Node', () => {
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Manage group opens with the correct group selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const makeGroup = async (name, type1, type2) => {
|
||||
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
|
||||
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
|
||||
await node1.click('title')
|
||||
await node2.click('title', {
|
||||
modifiers: ['Shift']
|
||||
})
|
||||
return await node2.convertToGroupNode(name)
|
||||
}
|
||||
|
||||
const group1 = await makeGroup(
|
||||
'g1',
|
||||
'CLIPTextEncode',
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
|
||||
|
||||
const manage1 = await group1.manageGroupNode()
|
||||
await comfyPage.nextFrame()
|
||||
expect(await manage1.getSelectedNodeType()).toBe('g1')
|
||||
await manage1.close()
|
||||
await expect(manage1.root).not.toBeVisible()
|
||||
|
||||
const manage2 = await group2.manageGroupNode()
|
||||
expect(await manage2.getSelectedNodeType()).toBe('g2')
|
||||
})
|
||||
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -146,7 +174,9 @@ test.describe('Group Node', () => {
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('legacy_group_node')
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.comfy-missing-nodes')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Copy and paste', () => {
|
||||
@@ -192,13 +222,6 @@ test.describe('Group Node', () => {
|
||||
await groupNode.copy()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.page.evaluate((groupNodeName) => {
|
||||
window['LiteGraph'].unregisterNodeType(groupNodeName)
|
||||
}, GROUP_NODE_TYPE)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node within the same workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -252,4 +275,20 @@ test.describe('Group Node', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Convert to group node, no selection', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
@@ -1,12 +1,14 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
header: Locator
|
||||
|
||||
constructor(
|
||||
readonly page: Page,
|
||||
readonly root: Locator
|
||||
) {
|
||||
this.footer = root.locator('footer')
|
||||
this.header = root.locator('header')
|
||||
}
|
||||
|
||||
async setLabel(name: string, label: string) {
|
||||
@@ -23,6 +25,11 @@ export class ManageGroupNode {
|
||||
await this.footer.getByText('Close').click()
|
||||
}
|
||||
|
||||
async getSelectedNodeType() {
|
||||
const select = this.header.locator('select').first()
|
||||
return await select.inputValue()
|
||||
}
|
||||
|
||||
async selectNode(name: string) {
|
||||
const list = this.root.locator('.comfy-group-manage-list-items')
|
||||
const item = list.getByText(name)
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('mixed_graph_items')
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
|
||||
await comfyPage.canvas.press('Delete')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
|
||||
})
|
||||
|
||||
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('mixed_graph_items')
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
await comfyPage.canvas.press('KeyP')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('pinned-all.png')
|
||||
await comfyPage.canvas.press('KeyP')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('unpinned-all.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Node Interaction', () => {
|
||||
test('Can enter prompt', async ({ comfyPage }) => {
|
||||
@@ -12,7 +33,7 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test.describe('Node Selection', () => {
|
||||
const multiSelectModifiers = ['Control', 'Shift', 'Meta']
|
||||
const multiSelectModifiers = ['Control', 'Shift', 'Meta'] as const
|
||||
|
||||
multiSelectModifiers.forEach((modifier) => {
|
||||
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
|
||||
@@ -73,6 +94,8 @@ test.describe('Node Interaction', () => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
||||
await comfyPage.connectEdge()
|
||||
// Move mouse to empty area to avoid slot highlight.
|
||||
await comfyPage.moveMouseToEmptyArea()
|
||||
// Litegraph renders edge with a slight offset.
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
|
||||
maxDiffPixels: 50
|
||||
@@ -114,6 +137,24 @@ test.describe('Node Interaction', () => {
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
|
||||
})
|
||||
|
||||
test('Auto snap&highlight when dragging link over node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
|
||||
await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true)
|
||||
|
||||
await comfyPage.page.mouse.move(
|
||||
comfyPage.clipTextEncodeNode1InputSlot.x,
|
||||
comfyPage.clipTextEncodeNode1InputSlot.y
|
||||
)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(
|
||||
comfyPage.clipTextEncodeNode2InputSlot.x,
|
||||
comfyPage.clipTextEncodeNode2InputSlot.y
|
||||
)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
|
||||
})
|
||||
})
|
||||
|
||||
test('Can adjust widget value', async ({ comfyPage }) => {
|
||||
@@ -244,7 +285,8 @@ test.describe('Node Interaction', () => {
|
||||
position: {
|
||||
x: 50,
|
||||
y: 10
|
||||
}
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
await comfyPage.page.keyboard.type('Hello World')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
@@ -259,7 +301,8 @@ test.describe('Node Interaction', () => {
|
||||
position: {
|
||||
x: 50,
|
||||
y: 50
|
||||
}
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
|
||||
})
|
||||
@@ -277,6 +320,15 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
|
||||
})
|
||||
|
||||
test('Can fit group to contents', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
|
||||
})
|
||||
|
||||
// Somehow this test fails on GitHub Actions. It works locally.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
|
||||
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
|
||||
@@ -311,7 +363,8 @@ test.describe('Group Interaction', () => {
|
||||
position: {
|
||||
x: 50,
|
||||
y: 10
|
||||
}
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
await comfyPage.page.keyboard.type('Hello World')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
@@ -463,7 +516,7 @@ test.describe('Widget Interaction', () => {
|
||||
await expect(textBox).toHaveValue('')
|
||||
await textBox.fill('Hello World')
|
||||
await expect(textBox).toHaveValue('Hello World')
|
||||
await comfyPage.ctrlZ()
|
||||
await comfyPage.ctrlZ(null)
|
||||
await expect(textBox).toHaveValue('')
|
||||
})
|
||||
|
||||
@@ -474,9 +527,9 @@ test.describe('Widget Interaction', () => {
|
||||
await textBox.fill('1girl')
|
||||
await expect(textBox).toHaveValue('1girl')
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlArrowUp()
|
||||
await comfyPage.ctrlArrowUp(null)
|
||||
await expect(textBox).toHaveValue('(1girl:1.05)')
|
||||
await comfyPage.ctrlZ()
|
||||
await comfyPage.ctrlZ(null)
|
||||
await expect(textBox).toHaveValue('1girl')
|
||||
})
|
||||
})
|
||||
@@ -486,6 +539,41 @@ test.describe('Load workflow', () => {
|
||||
await comfyPage.loadWorkflow('string_node_id')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
|
||||
})
|
||||
|
||||
test('Can load workflow with ("STRING",) input node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('string_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
|
||||
})
|
||||
|
||||
test('Restore workflow on reload (switch workflow)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
await comfyPage.reload({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
})
|
||||
|
||||
test('Restore workflow on reload (modify workflow)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('collapse')
|
||||
// Wait 300ms between 2 clicks so that it is not treated as a double click
|
||||
// by litegraph.
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
await comfyPage.reload({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
@@ -493,10 +581,6 @@ test.describe('Load duplicate workflow', () => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('A workflow can be loaded multiple times in a row', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |