Compare commits
524 Commits
v1.15.9
...
bl-right-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c177121a6 | ||
|
|
63181a1ddd | ||
|
|
e17ca7ce71 | ||
|
|
77d2cae301 | ||
|
|
164a4c4c25 | ||
|
|
47145ce4b8 | ||
|
|
6cf77a9814 | ||
|
|
886e4908d4 | ||
|
|
24cbc41832 | ||
|
|
a80a939324 | ||
|
|
8e2d7cabba | ||
|
|
e8dd26ff59 | ||
|
|
3a1bd1829a | ||
|
|
2f9dcd1669 | ||
|
|
e23547dd5a | ||
|
|
f0f40bc39b | ||
|
|
4b32786ef5 | ||
|
|
9942b17388 | ||
|
|
b99214bf5e | ||
|
|
2ef760c599 | ||
|
|
429ab6c365 | ||
|
|
b7693ae9f5 | ||
|
|
ebedf1074d | ||
|
|
0832347f47 | ||
|
|
c745af0f25 | ||
|
|
8c05266b83 | ||
|
|
fa14ec52f4 | ||
|
|
ec9da0b6c5 | ||
|
|
98bb1df436 | ||
|
|
75077fe9ed | ||
|
|
d5ecfb2c99 | ||
|
|
3211875084 | ||
|
|
a6bd04f951 | ||
|
|
5b32d2aad0 | ||
|
|
23ba7e6501 | ||
|
|
1e2b16f14d | ||
|
|
ec27d50333 | ||
|
|
693e156ab2 | ||
|
|
8274df5075 | ||
|
|
55bf36564d | ||
|
|
48ac4a2b36 | ||
|
|
c9c1275e4c | ||
|
|
78ebc54ebe | ||
|
|
88f2cc7847 | ||
|
|
7907e206da | ||
|
|
c4fa3dfe5a | ||
|
|
587d7a19a1 | ||
|
|
9ca705381c | ||
|
|
a937ac59ad | ||
|
|
995979a4e1 | ||
|
|
c02ac95815 | ||
|
|
d01926b043 | ||
|
|
344c6f6244 | ||
|
|
b2918a4cf6 | ||
|
|
6d4eafb07a | ||
|
|
97edaade63 | ||
|
|
83af274339 | ||
|
|
f251af25cc | ||
|
|
e2024c1e79 | ||
|
|
e8236e1a85 | ||
|
|
79a63de70e | ||
|
|
3eee7cde0b | ||
|
|
6bbe46009b | ||
|
|
1ca71caf45 | ||
|
|
65289b1927 | ||
|
|
9e2180dcd8 | ||
|
|
defea56ba5 | ||
|
|
e6bca95a5f | ||
|
|
841e3f743a | ||
|
|
73be826956 | ||
|
|
398dc6d8a6 | ||
|
|
d1f4341319 | ||
|
|
8c8bb1a3b7 | ||
|
|
05ef25a7a3 | ||
|
|
86aeeb87bb | ||
|
|
f7093f6ce0 | ||
|
|
88817e5bc0 | ||
|
|
3ac8aa248c | ||
|
|
75ab54ee04 | ||
|
|
a5729c9e06 | ||
|
|
d1da3476da | ||
|
|
ac01bff67e | ||
|
|
ec4ced26e7 | ||
|
|
40cfc43c54 | ||
|
|
35a811c5cf | ||
|
|
3d4ac07957 | ||
|
|
54055e7707 | ||
|
|
69f33f322f | ||
|
|
b81c2f7cd2 | ||
|
|
6289ac9182 | ||
|
|
86a7dd05a3 | ||
|
|
dee00edc5f | ||
|
|
afac449f41 | ||
|
|
aca1a2a194 | ||
|
|
4dfe75d68b | ||
|
|
2c37dba143 | ||
|
|
3936454ffd | ||
|
|
30ee669f5c | ||
|
|
811ddd6165 | ||
|
|
0cdaa512c8 | ||
|
|
3a514ca63b | ||
|
|
405b5fc5b7 | ||
|
|
0eaf7d11b6 | ||
|
|
fa58c04b3a | ||
|
|
9c84c9e250 | ||
|
|
6f9f048b4a | ||
|
|
768faeee7e | ||
|
|
eba81efb4b | ||
|
|
f9d92b8198 | ||
|
|
c4bbe7fee1 | ||
|
|
8f4f5f8e5f | ||
|
|
9e137d9924 | ||
|
|
a084b55db7 | ||
|
|
835f318999 | ||
|
|
c35d44c491 | ||
|
|
38d3e15103 | ||
|
|
674d04c9cf | ||
|
|
8209765eec | ||
|
|
9d48638464 | ||
|
|
0095f02f46 | ||
|
|
178c79e559 | ||
|
|
7c0040bfec | ||
|
|
77f91dea10 | ||
|
|
4ad6475283 | ||
|
|
b531d34027 | ||
|
|
55ad207345 | ||
|
|
ccc1039abb | ||
|
|
49400c69b6 | ||
|
|
32605eeb8f | ||
|
|
f08ec0a981 | ||
|
|
356886dc29 | ||
|
|
f96de24a66 | ||
|
|
9d48487af8 | ||
|
|
69b534bf14 | ||
|
|
2acb2ac181 | ||
|
|
37a583e39c | ||
|
|
d8821db2be | ||
|
|
f2c4e567e4 | ||
|
|
fec4c4e928 | ||
|
|
49d32f4809 | ||
|
|
07f0b88e30 | ||
|
|
d92ed22908 | ||
|
|
24c0c2c499 | ||
|
|
774bff2ed6 | ||
|
|
6d87f2b2ff | ||
|
|
20911aa892 | ||
|
|
3a6018589e | ||
|
|
4c92a7142e | ||
|
|
293993e7de | ||
|
|
a7ee3fae05 | ||
|
|
22dc84324e | ||
|
|
e76e9ec61a | ||
|
|
94fde504d0 | ||
|
|
e3ecf90bb3 | ||
|
|
a131f36cf3 | ||
|
|
4cad1a9567 | ||
|
|
47a6c6d595 | ||
|
|
068279ec34 | ||
|
|
2885ebf5e0 | ||
|
|
d4e76ddc45 | ||
|
|
9a5b80a279 | ||
|
|
985dab7e1c | ||
|
|
7f2b8a5321 | ||
|
|
59ce169ec9 | ||
|
|
4294b2c13b | ||
|
|
242c7e2885 | ||
|
|
c1442ec755 | ||
|
|
ebd9c96a28 | ||
|
|
e6d649b596 | ||
|
|
b037ba84e3 | ||
|
|
7c5c47c105 | ||
|
|
b152f67d95 | ||
|
|
be84d81c32 | ||
|
|
a474a094f3 | ||
|
|
bc360eef15 | ||
|
|
a52cc0ebe9 | ||
|
|
b3c6513e7a | ||
|
|
a9bdc70e28 | ||
|
|
58906fa821 | ||
|
|
a17fb04f83 | ||
|
|
5c0ad994d8 | ||
|
|
31be0a04f0 | ||
|
|
d9ab4270d1 | ||
|
|
36bd1f74ca | ||
|
|
7144ec54aa | ||
|
|
b2f144c27b | ||
|
|
014c0022c1 | ||
|
|
5d556c9c94 | ||
|
|
992c2ba822 | ||
|
|
4cc6a15fde | ||
|
|
3f50b8b46d | ||
|
|
bb588ff44e | ||
|
|
974236ce5a | ||
|
|
6ed870d431 | ||
|
|
4e25a78d2d | ||
|
|
6408623b71 | ||
|
|
fdad2475ce | ||
|
|
5486fb94a0 | ||
|
|
34b1fd5a72 | ||
|
|
aa46524829 | ||
|
|
3bd87820eb | ||
|
|
0f95ed852e | ||
|
|
3501b480d4 | ||
|
|
5fa0401acd | ||
|
|
4d39dc28e0 | ||
|
|
2a297e512d | ||
|
|
2019c1d877 | ||
|
|
2425e32d51 | ||
|
|
b6466c44e5 | ||
|
|
13441add24 | ||
|
|
64ce8ce5ed | ||
|
|
841ff0b46c | ||
|
|
df86da3d67 | ||
|
|
3aea2c120a | ||
|
|
6fdef0308b | ||
|
|
973a1eb0a9 | ||
|
|
b9d9ce78f9 | ||
|
|
bb1ac32ccd | ||
|
|
1ef3c007e6 | ||
|
|
db81b62274 | ||
|
|
43365b4318 | ||
|
|
dfdffcd27e | ||
|
|
252454484f | ||
|
|
c1984f7ccd | ||
|
|
bbbf140b1f | ||
|
|
754eb807de | ||
|
|
fad6c6c502 | ||
|
|
926278e9ef | ||
|
|
131fae72d4 | ||
|
|
a8a7288e0d | ||
|
|
2a21dd1004 | ||
|
|
3bdd814b3c | ||
|
|
0da6be5cdc | ||
|
|
6a3cf9e698 | ||
|
|
6b98f1de0e | ||
|
|
d05a340949 | ||
|
|
ca16e55579 | ||
|
|
a54529da40 | ||
|
|
f52915a590 | ||
|
|
13b00cdbc6 | ||
|
|
626b54da27 | ||
|
|
c24472aeac | ||
|
|
ea605887fa | ||
|
|
6edea73bf8 | ||
|
|
6b8bf989f1 | ||
|
|
3758a435cb | ||
|
|
813bf11484 | ||
|
|
b5ae354bec | ||
|
|
7a1a626b36 | ||
|
|
63d24301a3 | ||
|
|
93d7d2c69c | ||
|
|
2d9a0d02ab | ||
|
|
6601cf6959 | ||
|
|
77ac4a415c | ||
|
|
8ae36e2c8d | ||
|
|
cb9ec354e7 | ||
|
|
8713cd6e25 | ||
|
|
c30244ef66 | ||
|
|
2024aa6562 | ||
|
|
00c2181bb6 | ||
|
|
743f3cb5a1 | ||
|
|
111fdcc71a | ||
|
|
ba3b1bae87 | ||
|
|
23d32282bc | ||
|
|
197f33ffcd | ||
|
|
b618ebe36d | ||
|
|
b61640c51b | ||
|
|
869f500d4e | ||
|
|
ec8ee49a2c | ||
|
|
53372110d3 | ||
|
|
1cbc0fc613 | ||
|
|
c82401c61c | ||
|
|
2c75948ab9 | ||
|
|
89b826a231 | ||
|
|
3806ba3bf1 | ||
|
|
878aedb4f7 | ||
|
|
f7e4ed23d7 | ||
|
|
4461210f43 | ||
|
|
f868fac6e9 | ||
|
|
f7b51bbf7d | ||
|
|
8b6758ddfc | ||
|
|
0d87f301b9 | ||
|
|
8349181321 | ||
|
|
98b75e4819 | ||
|
|
d41b8c4e83 | ||
|
|
ee8f0bf013 | ||
|
|
834d5820d2 | ||
|
|
a43d1e1ee8 | ||
|
|
35f0551244 | ||
|
|
838acefd72 | ||
|
|
df723b56da | ||
|
|
c5eb97e563 | ||
|
|
98d7256da4 | ||
|
|
4828af9a13 | ||
|
|
4e0950d953 | ||
|
|
beec20a382 | ||
|
|
b4731637d4 | ||
|
|
30ce4a243d | ||
|
|
a2f04844e0 | ||
|
|
1531025070 | ||
|
|
fb14d24047 | ||
|
|
04815605b4 | ||
|
|
c7435af51b | ||
|
|
3bc9a3933f | ||
|
|
bd1890a422 | ||
|
|
5ec4ec8303 | ||
|
|
b1a98437e4 | ||
|
|
96d954ceb2 | ||
|
|
c6cb3f0a33 | ||
|
|
25136cc63d | ||
|
|
8472bde5dd | ||
|
|
3b6dc28727 | ||
|
|
6835b2bb5c | ||
|
|
8f540e6603 | ||
|
|
99cc587abf | ||
|
|
82c5f02c3d | ||
|
|
04f447c2a3 | ||
|
|
84c14ddd92 | ||
|
|
c7e6d66d47 | ||
|
|
5d4be8dc63 | ||
|
|
0bec26ca4b | ||
|
|
45eb4701d2 | ||
|
|
25359575db | ||
|
|
d0b99b95c6 | ||
|
|
ddff592561 | ||
|
|
8491ca91b7 | ||
|
|
630fa04882 | ||
|
|
bf80ae7295 | ||
|
|
44348180f5 | ||
|
|
4e12800336 | ||
|
|
d883448b86 | ||
|
|
1c59e3b51b | ||
|
|
b79cbf69af | ||
|
|
b05407ffdd | ||
|
|
2a62f7ec7f | ||
|
|
bf757c11ef | ||
|
|
0ed29a198d | ||
|
|
31d5671f24 | ||
|
|
ba3e2edb8a | ||
|
|
9c2300d780 | ||
|
|
3f85ff751c | ||
|
|
0caf1686c3 | ||
|
|
26f98d24fb | ||
|
|
fcbdee54ec | ||
|
|
a944372f39 | ||
|
|
df51e89311 | ||
|
|
b314435f81 | ||
|
|
ba367c0214 | ||
|
|
64ad6a9bb0 | ||
|
|
3819db5ec4 | ||
|
|
80517e8204 | ||
|
|
40034e77f9 | ||
|
|
2ef8b7cfd7 | ||
|
|
9cf3a0e568 | ||
|
|
f562cf27cd | ||
|
|
4244a0a258 | ||
|
|
ad3d2fe2e9 | ||
|
|
4c23cfbd4d | ||
|
|
9e10e55633 | ||
|
|
59cbe90fd3 | ||
|
|
16bd9abccd | ||
|
|
e84bdc96cf | ||
|
|
a57be36d4d | ||
|
|
3bd508c001 | ||
|
|
612500a4dc | ||
|
|
e9723407d8 | ||
|
|
a01aa39423 | ||
|
|
ab94a55858 | ||
|
|
1bcf5e28d4 | ||
|
|
9e247063aa | ||
|
|
cdddf359a8 | ||
|
|
8558f87547 | ||
|
|
262991db6b | ||
|
|
585d52e24e | ||
|
|
b7535755f0 | ||
|
|
6b7b0f6ec1 | ||
|
|
c7318bcf0a | ||
|
|
11f909436c | ||
|
|
d8f4dc95bb | ||
|
|
c1bc664edd | ||
|
|
e7fe2046ba | ||
|
|
bf4ad38e9b | ||
|
|
2b024bb186 | ||
|
|
6e5930c355 | ||
|
|
6151d487c6 | ||
|
|
e027a9bf44 | ||
|
|
53ee5904e8 | ||
|
|
f82bb71b1e | ||
|
|
40d08a890d | ||
|
|
ebf3c0c049 | ||
|
|
e77d5c1f57 | ||
|
|
b5c1da22db | ||
|
|
0006dd3855 | ||
|
|
7355209c12 | ||
|
|
2aef0a9af8 | ||
|
|
b74887d543 | ||
|
|
bf4ae227b3 | ||
|
|
184bb582da | ||
|
|
3bc3179763 | ||
|
|
eb100894ce | ||
|
|
9a992cb14d | ||
|
|
133aa9bc87 | ||
|
|
3204637e5a | ||
|
|
b2cb719026 | ||
|
|
add805460c | ||
|
|
9621b8f339 | ||
|
|
6be381b15d | ||
|
|
8afe99f48c | ||
|
|
9cd11261f9 | ||
|
|
fbc6665ff4 | ||
|
|
2daa51421c | ||
|
|
0f175c3dc1 | ||
|
|
8d4263c94e | ||
|
|
04580ac031 | ||
|
|
cd35f1d86d | ||
|
|
5d584577fe | ||
|
|
10a96d1af6 | ||
|
|
03392a3cc7 | ||
|
|
12576243ad | ||
|
|
e2a6dc2ec8 | ||
|
|
2f77d74891 | ||
|
|
dacb59f5d3 | ||
|
|
74f991ec1b | ||
|
|
6bc03a624e | ||
|
|
1fb015e046 | ||
|
|
87bf2310b6 | ||
|
|
f1a25989d7 | ||
|
|
236e3fb3e9 | ||
|
|
50382827bc | ||
|
|
41675805b6 | ||
|
|
6321fae6f3 | ||
|
|
06caa21a4d | ||
|
|
9ce3cccfd4 | ||
|
|
9935b322f0 | ||
|
|
60dd242b23 | ||
|
|
cec0dcbccd | ||
|
|
907632a250 | ||
|
|
45c450cdb9 | ||
|
|
ca85b2b144 | ||
|
|
1f28e6ef33 | ||
|
|
fee444c64b | ||
|
|
851739a768 | ||
|
|
1631665efb | ||
|
|
1a066c7062 | ||
|
|
e45f5bdebb | ||
|
|
c270e7734a | ||
|
|
8d7a21e008 | ||
|
|
29e63baca6 | ||
|
|
b22713daf0 | ||
|
|
c8b8953e0a | ||
|
|
731ce8599d | ||
|
|
ec8e55c1c1 | ||
|
|
04d38f2538 | ||
|
|
1c41db75f8 | ||
|
|
c7a7397000 | ||
|
|
e660e1d678 | ||
|
|
fb19752389 | ||
|
|
d098d6ae4e | ||
|
|
e4a5355f58 | ||
|
|
42c004d41d | ||
|
|
009c389607 | ||
|
|
b449dbd26b | ||
|
|
67835edfca | ||
|
|
60c0ce228a | ||
|
|
1990f25638 | ||
|
|
30c473db77 | ||
|
|
2371288fed | ||
|
|
2337fe6f8e | ||
|
|
25e6386b2a | ||
|
|
a03841cb1a | ||
|
|
dc5d7ea1be | ||
|
|
59e20964a0 | ||
|
|
8f00d8ca6a | ||
|
|
05e0036898 | ||
|
|
9e7690405a | ||
|
|
d687ea2cde | ||
|
|
c801a0c854 | ||
|
|
615c183059 | ||
|
|
27c8389b9f | ||
|
|
261f671ef0 | ||
|
|
22ae30132c | ||
|
|
7d3bf372b0 | ||
|
|
cd35373c25 | ||
|
|
a500a96c4a | ||
|
|
dc9ea44f3a | ||
|
|
2dc33b1eb9 | ||
|
|
ed8f9a5a4f | ||
|
|
6e72e1924e | ||
|
|
f7854a4e0b | ||
|
|
05023b7889 | ||
|
|
609496957b | ||
|
|
a879f413bb | ||
|
|
21d679a662 | ||
|
|
34f9603961 | ||
|
|
cf27a896f3 | ||
|
|
e9a98161ca | ||
|
|
fa132e4106 | ||
|
|
a489c19b07 | ||
|
|
46af2f03f3 | ||
|
|
3a1c95fb10 | ||
|
|
7a6f0e210e | ||
|
|
ac3bd7a848 | ||
|
|
77b5e487cf | ||
|
|
a7a8459e18 | ||
|
|
65c9c264c6 | ||
|
|
8ea070df12 | ||
|
|
2c02d4ebb3 | ||
|
|
a2b3048b94 | ||
|
|
549a42716f | ||
|
|
fa75614dc3 | ||
|
|
ac53296b2e | ||
|
|
6eb2b76621 | ||
|
|
9dd3b9fff5 | ||
|
|
785cad70ba | ||
|
|
026f076b8a | ||
|
|
65f1561ec6 | ||
|
|
bb094cf0ae | ||
|
|
ec684ee6b8 | ||
|
|
3978613f14 | ||
|
|
0a40e07f7e | ||
|
|
577af51ff8 | ||
|
|
df7c7383e2 | ||
|
|
1279f30f5a | ||
|
|
9ab4b549c0 |
43
.claude/commands/add-missing-i18n.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Add Missing i18n Translations
|
||||
|
||||
## Task: Add English translations for all new localized strings
|
||||
|
||||
### Step 1: Identify new translation keys
|
||||
Find all translation keys that were added in the current branch's changes. These keys appear as arguments to translation functions: `t()`, `st()`, `$t()`, or similar i18n functions.
|
||||
|
||||
### Step 2: Add translations to English locale file
|
||||
For each new translation key found, add the corresponding English text to the file `src/locales/en/main.json`.
|
||||
|
||||
### Key-to-JSON mapping rules:
|
||||
- Translation keys use dot notation to represent nested JSON structure
|
||||
- Convert dot notation to nested JSON objects when adding to the locale file
|
||||
- Example: The key `g.user.name` maps to:
|
||||
```json
|
||||
{
|
||||
"g": {
|
||||
"user": {
|
||||
"name": "User Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important notes:
|
||||
1. **Only modify the English locale file** (`src/locales/en/main.json`)
|
||||
2. **Do not modify other locale files** - translations for other languages are automatically generated by the `i18n.yaml` workflow
|
||||
3. **Exception for manual translations**: Only add translations to non-English locale files if:
|
||||
- You have specific domain knowledge that would produce a more accurate translation than the automated system
|
||||
- The automated translation would likely be incorrect due to technical terminology or context-specific meaning
|
||||
|
||||
### Example workflow:
|
||||
1. If you added `t('settings.advanced.enable')` in a Vue component
|
||||
2. Add to `src/locales/en/main.json`:
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"advanced": {
|
||||
"enable": "Enable advanced settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
57
.claude/commands/verify-visually.md
Normal file
@@ -0,0 +1,57 @@
|
||||
Your task is to perform visual verification of our recent changes to ensure they display correctly in the browser. This verification is critical for catching visual regressions, layout issues, and ensuring our UI changes render properly for end users.
|
||||
|
||||
<instructions>
|
||||
Follow these steps systematically to verify our changes:
|
||||
|
||||
1. **Server Setup**
|
||||
- Check if the dev server is running on port 5173 using browser navigation or port checking
|
||||
- If not running, start it with `npm run dev` from the root directory
|
||||
- If the server fails to start, provide detailed troubleshooting steps by reading package.json and README.md for accurate instructions
|
||||
- Wait for the server to be fully ready before proceeding
|
||||
|
||||
2. **Visual Testing Process**
|
||||
- Navigate to http://localhost:5173/
|
||||
- For each target page (specified in arguments or recently changed files):
|
||||
* Navigate to the page using direct URL or site navigation
|
||||
* Take a high-quality screenshot
|
||||
* Analyze the screenshot for the specific changes we implemented
|
||||
* Document any visual issues or improvements needed
|
||||
|
||||
3. **Quality Verification**
|
||||
Check each page for:
|
||||
- Content accuracy and completeness
|
||||
- Proper styling and layout alignment
|
||||
- Responsive design elements
|
||||
- Navigation functionality
|
||||
- Image loading and display
|
||||
- Typography and readability
|
||||
- Color scheme consistency
|
||||
- Interactive elements (buttons, links, forms)
|
||||
</instructions>
|
||||
|
||||
<examples>
|
||||
Common issues to watch for:
|
||||
- Broken layouts or overlapping elements
|
||||
- Missing images or broken image links
|
||||
- Inconsistent styling or spacing
|
||||
- Navigation menu problems
|
||||
- Mobile responsiveness issues
|
||||
- Text overflow or truncation
|
||||
- Color contrast problems
|
||||
</examples>
|
||||
|
||||
<reporting>
|
||||
For each page tested, provide:
|
||||
1. Page URL and screenshot
|
||||
2. Confirmation that changes display correctly OR detailed description of issues found
|
||||
3. Any design improvement suggestions
|
||||
4. Overall assessment of visual quality
|
||||
|
||||
If you find issues, be specific about:
|
||||
- Exact location of the problem
|
||||
- Expected vs actual behavior
|
||||
- Severity level (critical, important, minor)
|
||||
- Suggested fix if obvious
|
||||
</reporting>
|
||||
|
||||
Remember: Take your time with each screenshot and analysis. Visual quality directly impacts user experience and our project's professional appearance.
|
||||
60
.cursorrules
@@ -1,17 +1,25 @@
|
||||
// Vue 3 Composition API .cursorrules
|
||||
# Vue 3 Composition API Project Rules
|
||||
|
||||
// Vue 3 Composition API best practices
|
||||
const vue3CompositionApiBestPractices = [
|
||||
"Use setup() function for component logic",
|
||||
"Utilize ref and reactive for reactive state",
|
||||
"Implement computed properties with computed()",
|
||||
"Use watch and watchEffect for side effects",
|
||||
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
||||
"Utilize provide/inject for dependency injection",
|
||||
]
|
||||
## Vue 3 Composition API Best Practices
|
||||
- Use setup() function for component logic
|
||||
- Utilize ref and reactive for reactive state
|
||||
- Implement computed properties with computed()
|
||||
- Use watch and watchEffect for side effects
|
||||
- Implement lifecycle hooks with onMounted, onUpdated, etc.
|
||||
- Utilize provide/inject for dependency injection
|
||||
- Use vue 3.5 style of default prop declaration. Example:
|
||||
|
||||
// Folder structure
|
||||
const folderStructure = `
|
||||
```typescript
|
||||
const { nodes, showTotal = true } = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
}>()
|
||||
```
|
||||
|
||||
- Organize vue component in <template> <script> <style> order
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
src/
|
||||
components/
|
||||
constants/
|
||||
@@ -21,16 +29,25 @@ src/
|
||||
services/
|
||||
App.vue
|
||||
main.ts
|
||||
`;
|
||||
```
|
||||
|
||||
// Tailwind CSS best practices
|
||||
const tailwindCssBestPractices = [
|
||||
"Use Tailwind CSS for styling",
|
||||
"Implement responsive design with Tailwind CSS",
|
||||
]
|
||||
## Styling Guidelines
|
||||
- Use Tailwind CSS for styling
|
||||
- Implement responsive design with Tailwind CSS
|
||||
|
||||
// Additional instructions
|
||||
const additionalInstructions = `
|
||||
## PrimeVue Component Guidelines
|
||||
DO NOT use deprecated PrimeVue components. Use these replacements instead:
|
||||
- Dropdown → Use Select (import from 'primevue/select')
|
||||
- OverlayPanel → Use Popover (import from 'primevue/popover')
|
||||
- Calendar → Use DatePicker (import from 'primevue/datepicker')
|
||||
- InputSwitch → Use ToggleSwitch (import from 'primevue/toggleswitch')
|
||||
- Sidebar → Use Drawer (import from 'primevue/drawer')
|
||||
- Chips → Use AutoComplete with multiple enabled and typeahead disabled
|
||||
- TabMenu → Use Tabs without panels
|
||||
- Steps → Use Stepper without panels
|
||||
- InlineMessage → Use Message component
|
||||
|
||||
## Development Guidelines
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
@@ -40,4 +57,5 @@ const additionalInstructions = `
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
`;
|
||||
10. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json
|
||||
11. Never use deprecated PrimeVue components listed above
|
||||
|
||||
16
.env_example
@@ -1,4 +1,5 @@
|
||||
# Local development playwright target
|
||||
# Note: Don't add a trailing / after the port
|
||||
PLAYWRIGHT_TEST_URL=http://localhost:5173
|
||||
# PLAYWRIGHT_TEST_URL=http://localhost:8188
|
||||
|
||||
@@ -17,3 +18,18 @@ TEST_COMFYUI_DIR=/home/ComfyUI
|
||||
|
||||
# Whether to enable minification of the frontend code.
|
||||
ENABLE_MINIFY=true
|
||||
|
||||
# Whether to disable proxying the `/templates` route. If true, allows you to
|
||||
# serve templates from the ComfyUI_frontend/public/templates folder (for
|
||||
# locally testing changes to templates). When false or nonexistent, the
|
||||
# templates are served via the normal method from the server's python site
|
||||
# packages.
|
||||
DISABLE_TEMPLATES_PROXY=false
|
||||
|
||||
# If playwright tests are being run via vite dev server, Vue plugins will
|
||||
# invalidate screenshots. When `true`, vite plugins will not be loaded.
|
||||
DISABLE_VUE_PLUGINS=false
|
||||
|
||||
# Algolia credentials required for developing with the new custom node manager.
|
||||
ALGOLIA_APP_ID=4E0RO38HS8
|
||||
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -1,7 +1,9 @@
|
||||
name: Bug Report
|
||||
description: "Something is not behaving as expected."
|
||||
title: "[Bug]: "
|
||||
description: 'Something is not behaving as expected.'
|
||||
title: '[Bug]: '
|
||||
labels: ['Potential Bug']
|
||||
type: Bug
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -10,8 +12,15 @@ body:
|
||||
|
||||
- **1:** You are running the latest version of ComfyUI.
|
||||
- **2:** You have looked at the existing bug reports and made sure this isn't already reported.
|
||||
- **3:** You confirmed that the bug is not caused by a custom node. You can disable all custom nodes by passing
|
||||
`--disable-all-custom-nodes` command line argument.
|
||||
|
||||
- type: checkboxes
|
||||
id: custom-nodes-test
|
||||
attributes:
|
||||
label: Custom Node Testing
|
||||
description: Please confirm you have tried to reproduce the issue with all custom nodes disabled.
|
||||
options:
|
||||
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
@@ -1,7 +1,8 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature Request]: "
|
||||
labels: ["enhancement"]
|
||||
title: '[Feature Request]: '
|
||||
labels: ['enhancement']
|
||||
type: Feature
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
|
||||
37
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
Use the Vue 3 Composition API instead of the Options API when writing Vue components. An exception is when overriding or extending a PrimeVue component for compatibility, you may use the Options API.
|
||||
|
||||
Use setup() function for component logic
|
||||
|
||||
Utilize ref and reactive for reactive state
|
||||
|
||||
Implement computed properties with computed()
|
||||
|
||||
Use watch and watchEffect for side effects
|
||||
|
||||
Implement lifecycle hooks with onMounted, onUpdated, etc.
|
||||
|
||||
Utilize provide/inject for dependency injection
|
||||
|
||||
Use vue 3.5 style of default prop declaration.
|
||||
|
||||
Use Tailwind CSS for styling
|
||||
|
||||
Leverage VueUse functions for performance-enhancing styles
|
||||
|
||||
Use lodash for utility functions
|
||||
|
||||
Use TypeScript for type safety
|
||||
|
||||
Implement proper props and emits definitions
|
||||
|
||||
Utilize Vue 3's Teleport component when needed
|
||||
|
||||
Use Suspense for async components
|
||||
|
||||
Implement proper error handling
|
||||
|
||||
Follow Vue 3 style guide and naming conventions
|
||||
|
||||
Use Vite for fast development and building
|
||||
|
||||
Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
|
||||
72
.github/workflows/dev-release.yaml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Create Dev PyPI Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
devVersion:
|
||||
description: 'Dev version'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.current_version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
- name: Build project
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
npm run zipdist
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-files
|
||||
path: |
|
||||
dist/
|
||||
dist.zip
|
||||
|
||||
publish_pypi:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-files
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install build dependencies
|
||||
run: python -m pip install build
|
||||
- name: Setup pypi package
|
||||
run: |
|
||||
mkdir -p comfyui_frontend_package/comfyui_frontend_package/static/
|
||||
cp -r dist/* comfyui_frontend_package/comfyui_frontend_package/static/
|
||||
- name: Build pypi package
|
||||
run: python -m build
|
||||
working-directory: comfyui_frontend_package
|
||||
env:
|
||||
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
|
||||
- name: Publish pypi package
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
packages-dir: comfyui_frontend_package/dist
|
||||
2
.github/workflows/i18n-custom-nodes.yaml
vendored
@@ -136,7 +136,7 @@ jobs:
|
||||
git commit -m "Update locales"
|
||||
|
||||
- name: Install SSH key For PUSH
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
|
||||
with:
|
||||
# PR private key from action server
|
||||
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
|
||||
|
||||
2
.github/workflows/i18n-node-defs.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: "Update locales for node definitions"
|
||||
|
||||
6
.github/workflows/release.yaml
vendored
@@ -29,9 +29,9 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
npm run build
|
||||
npm run zipdist
|
||||
- name: Upload dist artifact
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
name: dist-files
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
env:
|
||||
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
|
||||
- name: Publish pypi package
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
packages-dir: comfyui_frontend_package/dist
|
||||
|
||||
13
.github/workflows/test-ui.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
ref: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be'
|
||||
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -39,7 +39,6 @@ jobs:
|
||||
- name: Build ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
npm run build
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
@@ -47,8 +46,8 @@ jobs:
|
||||
id: cache-key
|
||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache setup
|
||||
uses: actions/cache@v3
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
with:
|
||||
path: |
|
||||
ComfyUI
|
||||
@@ -63,9 +62,13 @@ jobs:
|
||||
matrix:
|
||||
browser: [chromium, chromium-2x, mobile-chrome]
|
||||
steps:
|
||||
- name: Wait for cache propagation
|
||||
run: sleep 10
|
||||
|
||||
- name: Restore cached setup
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
with:
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
ComfyUI
|
||||
ComfyUI_frontend
|
||||
|
||||
4
.github/workflows/update-electron-types.yaml
vendored
@@ -30,9 +30,9 @@ jobs:
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
title: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
body: |
|
||||
|
||||
2
.github/workflows/update-litegraph.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
|
||||
92
.github/workflows/update-manager-types.yaml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update ComfyUI-Manager API Types
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-manager-types:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Checkout ComfyUI-Manager repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: Comfy-Org/ComfyUI-Manager
|
||||
path: ComfyUI-Manager
|
||||
clean: true
|
||||
|
||||
- name: Get Manager commit information
|
||||
id: manager-info
|
||||
run: |
|
||||
cd ComfyUI-Manager
|
||||
MANAGER_COMMIT=$(git rev-parse --short HEAD)
|
||||
echo "commit=${MANAGER_COMMIT}" >> $GITHUB_OUTPUT
|
||||
cd ..
|
||||
|
||||
- name: Generate Manager API types
|
||||
run: |
|
||||
echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..."
|
||||
npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
|
||||
|
||||
- name: Validate generated types
|
||||
run: |
|
||||
if [ ! -f ./src/types/generatedManagerTypes.ts ]; then
|
||||
echo "Error: Types file was not generated."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if file is not empty
|
||||
if [ ! -s ./src/types/generatedManagerTypes.ts ]; then
|
||||
echo "Error: Generated types file is empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
if [[ -z $(git status --porcelain ./src/types/generatedManagerTypes.ts) ]]; then
|
||||
echo "No changes to ComfyUI-Manager API types detected."
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
else
|
||||
echo "Changes detected in ComfyUI-Manager API types."
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'
|
||||
title: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'
|
||||
body: |
|
||||
## Automated API Type Update
|
||||
|
||||
This PR updates the ComfyUI-Manager API types from the latest ComfyUI-Manager OpenAPI specification.
|
||||
|
||||
- Manager commit: ${{ steps.manager-info.outputs.commit }}
|
||||
- Generated on: ${{ github.event.repository.updated_at }}
|
||||
|
||||
These types are automatically generated using openapi-typescript.
|
||||
branch: update-manager-types-${{ steps.manager-info.outputs.commit }}
|
||||
base: main
|
||||
labels: Manager
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
src/types/generatedManagerTypes.ts
|
||||
2
.github/workflows/update-registry-types.yaml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'
|
||||
|
||||
2
.github/workflows/version-bump.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[release] Bump version to ${{ steps.bump-version.outputs.NEW_VERSION }}'
|
||||
|
||||
3
.gitignore
vendored
@@ -55,3 +55,6 @@ dist.zip
|
||||
|
||||
# Temporary repository directory
|
||||
templates_repo/
|
||||
|
||||
# Vite’s timestamped config modules
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4',
|
||||
modelName: 'gpt-4.1',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, controlnet, lora.
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
`
|
||||
|
||||
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@executeautomation/playwright-mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
25
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"austenc.tailwind-docs",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"esbenp.prettier-vscode",
|
||||
"figma.figma-vscode-extension",
|
||||
"github.vscode-github-actions",
|
||||
"github.vscode-pull-request-github",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"lokalise.i18n-ally",
|
||||
"ms-playwright.playwright",
|
||||
"vitest.explorer",
|
||||
"vue.volar",
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"deque-systems.vscode-axe-linter",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"donjayamanne.githistory",
|
||||
"wix.vscode-import-cost",
|
||||
"prograhammer.tslint-vue",
|
||||
"antfu.vite"
|
||||
]
|
||||
}
|
||||
56
CLAUDE.md
Normal file
@@ -0,0 +1,56 @@
|
||||
- use npm run to see what commands are available
|
||||
- After making code changes, follow this general process: (1) Create unit tests, component tests, browser tests (if appropriate for each), (2) run unit tests, component tests, and browser tests until passing, (3) run typecheck, lint, format (with prettier) -- you can use `npm run` command to see the scripts available, (4) check if any READMEs (including nested) or documentation needs to be updated, (5) Decide whether the changes are worth adding new content to the external documentation for (or would requires changes to the external documentation) at https://docs.comfy.org, then present your suggestion
|
||||
- When referencing PrimeVue, you can get all the docs here: https://primevue.org. Do this instead of making up or inferring names of Components
|
||||
- When trying to set tailwind classes for dark theme, use "dark-theme:" prefix rather than "dark:"
|
||||
- Never add lines to PR descriptions that say "Generated with Claude Code"
|
||||
- When making PR names and commit messages, if you are going to add a prefix like "docs:", "feat:", "bugfix:", use square brackets around the prefix term and do not use a colon (e.g., should be "[docs]" rather than "docs:").
|
||||
- When I reference GitHub Repos related to Comfy-Org, you should proactively fetch or read the associated information in the repo. To do so, you should exhaust all options: (1) Check if we have a local copy of the repo, (2) Use the GitHub API to fetch the information; you may want to do this IN ADDITION to the other options, especially for reading specific branches/PRs/comments/reviews/metadata, and (3) curl the GitHub website and parse the html or json responses
|
||||
- For information about ComfyUI, ComfyUI_frontend, or ComfyUI-Manager, you can web search or download these wikis: https://deepwiki.com/Comfy-Org/ComfyUI-Manager, https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview, https://deepwiki.com/comfyanonymous/ComfyUI/2-core-architecture
|
||||
- If a question/project is related to Comfy-Org, Comfy, or ComfyUI ecosystem, you should proactively use the Comfy docs to answer the question. The docs may be referenced with URLs like https://docs.comfy.org
|
||||
- When operating inside a repo, check for README files at key locations in the repo detailing info about the contents of that folder. E.g., top-level key folders like tests-ui, browser_tests, composables, extensions/core, stores, services often have their own README.md files. When writing code, make sure to frequently reference these README files to understand the overall architecture and design of the project. Pay close attention to the snippets to learn particular patterns that seem to be there for a reason, as you should emulate those.
|
||||
- Prefer running single tests, and not the whole test suite, for performance
|
||||
- If using a lesser known or complex CLI tool, run the --help to see the documentation before deciding what to run, even if just for double-checking or verifying things.
|
||||
- IMPORTANT: the most important goal when writing code is to create clean, best-practices, sustainable, and scalable public APIs and interfaces. Our app is used by thousands of users and we have thousands of mods/extensions that are constantly changing and updating; and we are also always updating. That's why it is IMPORTANT that we design systems and write code that follows practices of domain-driven design, object-oriented design, and design patterns (such that you can assure stability while allowing for all components around you to change and evolve). We ABSOLUTELY prioritize clean APIs and public interfaces that clearly define and restrict how/what the mods/extensions can access.
|
||||
- If any of these technologies are referenced, you can proactively read their docs at these locations: https://primevue.org/theming, https://primevue.org/forms/, https://www.electronjs.org/docs/latest/api/browser-window, https://vitest.dev/guide/browser/, https://atlassian.design/components/pragmatic-drag-and-drop/core-package/drop-targets/, https://playwright.dev/docs/api/class-test, https://playwright.dev/docs/api/class-electron, https://www.algolia.com/doc/api-reference/rest-api/, https://pyav.org/docs/develop/cookbook/basics.html
|
||||
- IMPORTANT: Never add Co-Authored by Claude or any reference to Claude or Claude Code in commit messages, PR descriptions, titles, or any documentation whatsoever
|
||||
- The npm script to type check is called "typecheck" NOT "type check"
|
||||
- Use the Vue 3 Composition API instead of the Options API when writing Vue components. An exception is when overriding or extending a PrimeVue component for compatibility, you may use the Options API.
|
||||
- when we are solving an issue we know the link/number for, we should add "Fixes #n" (where n is the issue number) to the PR description.
|
||||
- Never write css if you can accomplish the same thing with tailwind utility classes
|
||||
- Use setup() function for component logic
|
||||
- Utilize ref and reactive for reactive state
|
||||
- Implement computed properties with computed()
|
||||
- Use watch and watchEffect for side effects
|
||||
- Implement lifecycle hooks with onMounted, onUpdated, etc.
|
||||
- Utilize provide/inject for dependency injection
|
||||
- Use vue 3.5 style of default prop declaration. Do not define a `props` variable; instead, destructure props. Since vue 3.5, destructuring props does not strip them of reactivity.
|
||||
- Use Tailwind CSS for styling
|
||||
- Leverage VueUse functions for performance-enhancing styles
|
||||
- Use lodash for utility functions
|
||||
- Use TypeScript for type safety
|
||||
- Implement proper props and emits definitions
|
||||
- Utilize Vue 3's Teleport component when needed
|
||||
- Use Suspense for async components
|
||||
- Implement proper error handling
|
||||
- Follow Vue 3 style guide and naming conventions
|
||||
- Use Vite for fast development and building
|
||||
- Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
|
||||
- Avoid using `@ts-expect-error` to work around type issues. We needed to employ it to migrate to TypeScript, but it should not be viewed as an accepted practice or standard.
|
||||
- DO NOT use deprecated PrimeVue components. Use these replacements instead:
|
||||
* `Dropdown` → Use `Select` (import from 'primevue/select')
|
||||
* `OverlayPanel` → Use `Popover` (import from 'primevue/popover')
|
||||
* `Calendar` → Use `DatePicker` (import from 'primevue/datepicker')
|
||||
* `InputSwitch` → Use `ToggleSwitch` (import from 'primevue/toggleswitch')
|
||||
* `Sidebar` → Use `Drawer` (import from 'primevue/drawer')
|
||||
* `Chips` → Use `AutoComplete` with multiple enabled and typeahead disabled
|
||||
* `TabMenu` → Use `Tabs` without panels
|
||||
* `Steps` → Use `Stepper` without panels
|
||||
* `InlineMessage` → Use `Message` component
|
||||
* Use `api.apiURL()` for all backend API calls and routes
|
||||
- Actual API endpoints like /prompt, /queue, /view, etc.
|
||||
- Image previews: `api.apiURL('/view?...')`
|
||||
- Any backend-generated content or dynamic routes
|
||||
* Use `api.fileURL()` for static files served from the public folder:
|
||||
- Templates: `api.fileURL('/templates/default.json')`
|
||||
- Extensions: `api.fileURL(extensionPath)` for loading JS modules
|
||||
- Any static assets that exist in the public directory
|
||||
136
README.md
@@ -510,16 +510,62 @@ The selection toolbox will display the command button when items are selected:
|
||||
|
||||
</details>
|
||||
|
||||
## Contributing
|
||||
|
||||
We're building this frontend together and would love your help — no matter how you'd like to pitch in! You don't need to write code to make a difference.
|
||||
|
||||
Here are some ways to get involved:
|
||||
|
||||
- **Pull Requests:** Add features, fix bugs, or improve code health. Browse [issues](https://github.com/Comfy-Org/ComfyUI_frontend/issues) for inspiration.
|
||||
- **Vote on Features:** Give a 👍 to the feature requests you care about to help us prioritize.
|
||||
- **Verify Bugs:** Try reproducing reported issues and share your results (even if the bug doesn't occur!).
|
||||
- **Community Support:** Hop into our [Discord](https://www.comfy.org/discord) to answer questions or get help.
|
||||
- **Share & Advocate:** Tell your friends, tweet about us, or share tips to support the project.
|
||||
|
||||
Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
|
||||
## Development
|
||||
|
||||
### Tech Stack
|
||||
### Prerequisites & Technology 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.js](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
|
||||
- **Required Software**:
|
||||
- Node.js (v16 or later) and npm
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance
|
||||
|
||||
- **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.js](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
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/Comfy-Org/ComfyUI_frontend.git
|
||||
cd ComfyUI_frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Configure environment (optional):
|
||||
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
|
||||
|
||||
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
|
||||
|
||||
### Dev Server Configuration
|
||||
|
||||
To launch ComfyUI and have it connect to your development server:
|
||||
|
||||
```bash
|
||||
python main.py --port 8188
|
||||
```
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
@@ -557,6 +603,74 @@ After you start the dev server, you should see following logs:
|
||||
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.
|
||||
|
||||
### Recommended Code Editor Configuration
|
||||
|
||||
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
|
||||
|
||||
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
|
||||
### Recommended MCP Integrations
|
||||
|
||||
#### Playwright Browser Automation
|
||||
|
||||
The Playwright MCP server enables LLM coding assistants (like Copilot, Claude Code, etc.) to control and inspect web browsers programmatically. This allows assistants to navigate websites, take screenshots, and interact with web pages on your behalf.
|
||||
|
||||
For ComfyUI_frontend development, you can ask coding assistants to use Playwright screenshots to continuously verify that your code changes produce the expected visual results.
|
||||
|
||||
##### Setup for Claude Code
|
||||
|
||||
After installing dependencies with `npm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
|
||||
|
||||
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
|
||||
|
||||
|
||||
```
|
||||
> navigate to localhost:5173 and take a screenshot.
|
||||
tell me the items in the menubar
|
||||
|
||||
● I'll navigate to localhost:5173 and take a
|
||||
screenshot to see the menubar items.
|
||||
|
||||
● playwright:playwright_navigate (MCP)(url: …
|
||||
"http://local
|
||||
host:5173")
|
||||
⎿ Navigated to http://localhost:5173
|
||||
|
||||
● playwright:playwright_screenshot (MCP)(name: "lo…
|
||||
alhost_5173
|
||||
_menubar")
|
||||
|
||||
⎿ Screenshot saved to: ../../../Downloads/localho
|
||||
st_5173_menubar-2025-05-30T17-23-00-942Z.png
|
||||
⎿ Screenshot also stored in memory with name:
|
||||
'localhost_5173_menubar'
|
||||
|
||||
● Read(../../../Downloads/localhost_5173_menubar-2…
|
||||
25-05-30T17-23-00-942Z.png)
|
||||
⎿ Read image (94.3KB)
|
||||
|
||||
● Based on the screenshot, I can see the menubar at
|
||||
the top of the ComfyUI interface contains the
|
||||
following items:
|
||||
|
||||
Main Menubar Items:
|
||||
- Workflow - Menu for workflow-related actions
|
||||
- Edit - Menu for editing operations
|
||||
- Help - Menu for help and documentation
|
||||
|
||||
Additional UI Elements:
|
||||
- ComfyUI Logo - On the far left
|
||||
- Workflow Tab - Shows "Unsaved Workflow" with a
|
||||
dropdown and close button
|
||||
- Layout Controls - On the far right (grid view
|
||||
and hamburger menu icons)
|
||||
|
||||
The interface shows a typical ComfyUI workflow
|
||||
graph with nodes like "Load Checkpoint", "CLIP
|
||||
Text Encode (Prompt)", "KSampler", and "Empty
|
||||
Latent Image" connected with colored cables.
|
||||
```
|
||||
|
||||
### Unit Test
|
||||
|
||||
- `npm i` to install all dependencies
|
||||
@@ -586,3 +700,11 @@ This will replace the litegraph package in this repo with the local litegraph re
|
||||
### i18n
|
||||
|
||||
See [locales/README.md](src/locales/README.md) for details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For comprehensive troubleshooting and technical support, please refer to our official documentation:
|
||||
|
||||
- **[General Troubleshooting Guide](https://docs.comfy.org/troubleshooting/overview)** - Common issues, performance optimization, and reporting bugs
|
||||
- **[Custom Node Issues](https://docs.comfy.org/troubleshooting/custom-node-issues)** - Debugging custom node problems and conflicts
|
||||
- **[Desktop Installation Guide](https://docs.comfy.org/installation/desktop/windows)** - Desktop-specific installation and troubleshooting
|
||||
@@ -1,6 +1,6 @@
|
||||
# Playwright Testing for ComfyUI_frontend
|
||||
|
||||
This document outlines the setup and usage of Playwright for testing the ComfyUI_frontend project.
|
||||
This document outlines the setup, usage, and common patterns for Playwright browser tests in the ComfyUI_frontend project.
|
||||
|
||||
## WARNING
|
||||
|
||||
@@ -9,18 +9,29 @@ If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directo
|
||||
|
||||
## Setup
|
||||
|
||||
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
|
||||
ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing.
|
||||
### ComfyUI devtools
|
||||
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
|
||||
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
|
||||
|
||||
### Node.js & Playwright Prerequisites
|
||||
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
|
||||
|
||||
```bash
|
||||
npx playwright install chromium --with-deps
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
Ensure the environment variables in `.env` are set correctly according to your setup.
|
||||
|
||||
The `.env` file will not exist until you create it yourself.
|
||||
|
||||
A template with helpful information can be found in `.env_example`.
|
||||
|
||||
### Multiple Tests
|
||||
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
|
||||
|
||||
## Running Tests
|
||||
|
||||
There are two ways to run the tests:
|
||||
There are multiple ways to run the tests:
|
||||
|
||||
1. **Headless mode with report generation:**
|
||||
```bash
|
||||
@@ -34,18 +45,241 @@ 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.
|
||||
|
||||

|
||||
|
||||
3. **Running specific tests:**
|
||||
```bash
|
||||
npx playwright test widget.spec.ts
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
Browser tests in this project follow a specific organization pattern:
|
||||
|
||||
- **Fixtures**: Located in `fixtures/` - These provide test setup and utilities
|
||||
- `ComfyPage.ts` - The main fixture for interacting with ComfyUI
|
||||
- `ComfyMouse.ts` - Utility for mouse interactions with the canvas
|
||||
- Components fixtures in `fixtures/components/` - Page object models for UI components
|
||||
|
||||
- **Tests**: Located in `tests/` - The actual test specifications
|
||||
- Organized by functionality (e.g., `widget.spec.ts`, `interaction.spec.ts`)
|
||||
- Snapshot directories (e.g., `widget.spec.ts-snapshots/`) contain reference screenshots
|
||||
|
||||
- **Utilities**: Located in `utils/` - Common utility functions
|
||||
- `litegraphUtils.ts` - Utilities for working with LiteGraph nodes
|
||||
|
||||
## Writing Effective Tests
|
||||
|
||||
When writing new tests, follow these patterns:
|
||||
|
||||
### Test Structure
|
||||
|
||||
```typescript
|
||||
// Import the test fixture
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage';
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
// Set up test environment if needed
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Common setup
|
||||
});
|
||||
|
||||
test('should do something specific', async ({ comfyPage }) => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Leverage Existing Fixtures and Helpers
|
||||
|
||||
Always check for existing helpers and fixtures before implementing new ones:
|
||||
|
||||
- **ComfyPage**: Main fixture with methods for canvas interaction and node management
|
||||
- **ComfyMouse**: Helper for precise mouse operations on the canvas
|
||||
- **Helpers**: Check `browser_tests/helpers/` for specialized helpers like:
|
||||
- `actionbar.ts`: Interact with the action bar
|
||||
- `manageGroupNode.ts`: Group node management operations
|
||||
- `templates.ts`: Template workflows operations
|
||||
- **Component Fixtures**: Check `browser_tests/fixtures/components/` for UI component helpers
|
||||
- **Utility Functions**: Check `browser_tests/utils/` and `browser_tests/fixtures/utils/` for shared utilities
|
||||
|
||||
Most common testing needs are already addressed by these helpers, which will make your tests more consistent and reliable.
|
||||
|
||||
### Key Testing Patterns
|
||||
|
||||
1. **Focus elements explicitly**:
|
||||
Canvas-based elements often need explicit focus before interaction:
|
||||
```typescript
|
||||
// Click the canvas first to focus it before pressing keys
|
||||
await comfyPage.canvas.click();
|
||||
await comfyPage.page.keyboard.press('a');
|
||||
```
|
||||
|
||||
2. **Mark canvas as dirty if needed**:
|
||||
Some interactions need explicit canvas updates:
|
||||
```typescript
|
||||
// After programmatically changing node state, mark canvas dirty
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
```
|
||||
|
||||
3. **Use node references over coordinates**:
|
||||
Node references from `fixtures/utils/litegraphUtils.ts` provide stable ways to interact with nodes:
|
||||
```typescript
|
||||
// Prefer this:
|
||||
const node = await comfyPage.getNodeRefsByType('LoadImage')[0];
|
||||
await node.click('title');
|
||||
|
||||
// Over this:
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } });
|
||||
```
|
||||
|
||||
4. **Wait for canvas to render after UI interactions**:
|
||||
```typescript
|
||||
await comfyPage.nextFrame();
|
||||
```
|
||||
|
||||
5. **Clean up persistent server state**:
|
||||
While most state is reset between tests, anything stored on the server persists:
|
||||
```typescript
|
||||
// Reset settings that affect other tests (these are stored on server)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark');
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', 'None');
|
||||
|
||||
// Clean up uploaded files if needed
|
||||
await comfyPage.request.delete(`${comfyPage.url}/api/delete/image.png`);
|
||||
```
|
||||
|
||||
6. **Prefer functional assertions over screenshots**:
|
||||
Use screenshots only when visual verification is necessary:
|
||||
```typescript
|
||||
// Prefer this:
|
||||
expect(await node.isPinned()).toBe(true);
|
||||
expect(await node.getProperty('title')).toBe('Expected Title');
|
||||
|
||||
// Over this - only use when needed:
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('state.png');
|
||||
```
|
||||
|
||||
7. **Use minimal test workflows**:
|
||||
When creating test workflows, keep them as minimal as possible:
|
||||
```typescript
|
||||
// Include only the components needed for the test
|
||||
await comfyPage.loadWorkflow('single_ksampler');
|
||||
```
|
||||
|
||||
## Common Patterns and Utilities
|
||||
|
||||
### Page Object Pattern
|
||||
|
||||
Tests use the Page Object pattern to create abstractions over the UI:
|
||||
|
||||
```typescript
|
||||
// Using the ComfyPage fixture
|
||||
test('Can toggle boolean widget', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/boolean_widget')
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
const widget = await node.getWidget(0)
|
||||
await widget.click()
|
||||
});
|
||||
```
|
||||
|
||||
### Node References
|
||||
|
||||
The `NodeReference` class provides helpers for interacting with LiteGraph nodes:
|
||||
|
||||
```typescript
|
||||
// Getting node by type and interacting with it
|
||||
const nodes = await comfyPage.getNodeRefsByType('LoadImage')
|
||||
const loadImageNode = nodes[0]
|
||||
const widget = await loadImageNode.getWidget(0)
|
||||
await widget.click()
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Tests use screenshot comparisons to verify UI state:
|
||||
|
||||
```typescript
|
||||
// Take a screenshot and compare to reference
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget_toggled.png')
|
||||
```
|
||||
|
||||
### Waiting for Animations
|
||||
|
||||
Always call `nextFrame()` after actions that trigger animations:
|
||||
|
||||
```typescript
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
await comfyPage.nextFrame() // Wait for canvas to redraw
|
||||
```
|
||||
|
||||
### Mouse Interactions
|
||||
|
||||
Canvas operations use special helpers to ensure proper timing:
|
||||
|
||||
```typescript
|
||||
// Using ComfyMouse for drag and drop
|
||||
await comfyMouse.dragAndDrop(
|
||||
{ x: 100, y: 100 }, // From
|
||||
{ x: 200, y: 200 } // To
|
||||
)
|
||||
|
||||
// Standard ComfyPage helpers
|
||||
await comfyPage.drag({ x: 100, y: 100 }, { x: 200, y: 200 })
|
||||
await comfyPage.pan({ x: 200, y: 200 })
|
||||
await comfyPage.zoom(-100) // Zoom in
|
||||
```
|
||||
|
||||
### Workflow Management
|
||||
|
||||
Tests use workflows stored in `assets/` for consistent starting points:
|
||||
|
||||
```typescript
|
||||
// Load a test workflow
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
|
||||
// Wait for workflow to load and stabilize
|
||||
await comfyPage.nextFrame()
|
||||
```
|
||||
|
||||
### Custom Assertions
|
||||
|
||||
The project includes custom Playwright assertions through `comfyExpect`:
|
||||
|
||||
```typescript
|
||||
// Check if a node is in a specific state
|
||||
await expect(node).toBePinned()
|
||||
await expect(node).toBeBypassed()
|
||||
await expect(node).toBeCollapsed()
|
||||
```
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
### Flaky Tests
|
||||
|
||||
- **Timing Issues**: Always wait for animations to complete with `nextFrame()`
|
||||
- **Coordinate Sensitivity**: Canvas coordinates are viewport-relative; use node references when possible
|
||||
- **Test Isolation**: Tests run in parallel; avoid dependencies between tests
|
||||
- **Screenshots vary**: Ensure your OS and browser match the reference environment (Linux)
|
||||
- **Async / await**: Race conditions are a very common cause of test flakiness
|
||||
|
||||
## Screenshot Expectations
|
||||
|
||||
Due to variations in system font rendering, screenshot expectations are platform-specific. Please note:
|
||||
|
||||
- We maintain Linux screenshot expectations as our GitHub Action runner operates in a Linux environment.
|
||||
- To set new test expectations:
|
||||
1. Create a pull request from a `Comfy-Org/ComfyUI_frontend` branch.
|
||||
2. Add the `New Browser Test Expectation` tag to your pull request.
|
||||
3. This will trigger a GitHub action to update the screenshot expectations automatically.
|
||||
- **DO NOT commit local screenshot expectations** to the repository
|
||||
- We maintain Linux screenshot expectations as our GitHub Action runner operates in a Linux environment
|
||||
- While developing, you can generate local screenshots for your tests, but these will differ from CI-generated ones
|
||||
|
||||
> **Note:** If you're making a pull request from a forked repository, the GitHub action won't be able to commit updated screenshot expectations directly to your PR branch.
|
||||
To set new test expectations for PR:
|
||||
|
||||
1. Write your test with screenshot assertions using `toHaveScreenshot(filename)`
|
||||
2. Create a pull request from a `Comfy-Org/ComfyUI_frontend` branch
|
||||
3. Add the `New Browser Test Expectation` tag to your pull request
|
||||
4. The GitHub CI will automatically generate and commit the reference screenshots
|
||||
|
||||
This approach ensures consistent screenshot expectations across all PRs and avoids issues with platform-specific rendering.
|
||||
|
||||
> **Note:** If you're making a pull request from a forked repository, the GitHub action won't be able to commit updated screenshot expectations directly to your PR branch.
|
||||
BIN
browser_tests/assets/animated_webp.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
126
browser_tests/assets/bad_link.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
|
||||
"revision": 0,
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
867.4669799804688,
|
||||
347.22369384765625
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
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
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "steps"
|
||||
},
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "PrimitiveInt",
|
||||
"pos": [
|
||||
443.0852355957031,
|
||||
441.131591796875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
3
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveInt"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
3,
|
||||
5,
|
||||
0,
|
||||
4,
|
||||
5,
|
||||
"INT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.9487171000000016,
|
||||
"offset": [
|
||||
-325.57196748514497,
|
||||
-168.13150517966463
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -33,5 +33,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -130,6 +130,11 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
53
browser_tests/assets/default_input.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "9bcb9451-8319-492a-88d4-fb711d8c3d25",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 6,
|
||||
"type": "DevToolsNodeWithDefaultInput",
|
||||
"pos": [
|
||||
8.39722728729248,
|
||||
29.727279663085938
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "float_input",
|
||||
"shape": 7,
|
||||
"type": "FLOAT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithDefaultInput"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
82
browser_tests/assets/dynamically_added_input.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 13,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
0,
|
||||
30
|
||||
],
|
||||
"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
|
||||
} ,
|
||||
{
|
||||
"name": "dynamic_input",
|
||||
"type": "FLOAT",
|
||||
"link": null,
|
||||
"_meta": "Dynamically added input via frontend JS logic"
|
||||
}
|
||||
],
|
||||
"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": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -499,6 +499,11 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
85
browser_tests/assets/execution/partial_execution.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PreviewAny",
|
||||
"pos": [946.2566528320312, 598.4373168945312],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PreviewAny",
|
||||
"pos": [951.0236206054688, 421.3861083984375],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [575.1760864257812, 504.5214538574219],
|
||||
"size": [270, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2, 3]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": ["foo"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[2, 3, 0, 1, 0, "*"],
|
||||
[3, 3, 0, 4, 0, "*"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.1",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -160,4 +160,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
@@ -401,4 +405,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
74
browser_tests/assets/input_order_swap.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
|
||||
"revision": 0,
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [904, 466],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [""]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [556.8589477539062, 472.94342041015625],
|
||||
"size": [315, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [1]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": ["foo"]
|
||||
}
|
||||
],
|
||||
"links": [[1, 2, 0, 1, 0, "STRING"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.7715610000000013,
|
||||
"offset": [-388.521484375, -162.31336975097656]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -110,6 +110,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"hello": {
|
||||
"nodes": [
|
||||
@@ -249,4 +253,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
59
browser_tests/assets/model_metadata_widget_mismatch.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [256, 256],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
"models": [
|
||||
{
|
||||
"name": "outdated_model.safetensors",
|
||||
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
|
||||
"directory": "text_encoders"
|
||||
},
|
||||
{
|
||||
"name": "another_outdated_model.safetensors",
|
||||
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
|
||||
"directory": "text_encoders"
|
||||
}
|
||||
]
|
||||
},
|
||||
"widgets_values": ["current_selected_model.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
36
browser_tests/assets/node_with_v2_combo_input.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "5635564e-189f-49e4-9b25-6b7634bcd595",
|
||||
"revision": 0,
|
||||
"last_node_id": 78,
|
||||
"last_link_id": 53,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 78,
|
||||
"type": "DevToolsNodeWithV2ComboInput",
|
||||
"pos": [1320, 904],
|
||||
"size": [270.3199157714844, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "COMBO",
|
||||
"type": "COMBO",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithV2ComboInput"
|
||||
},
|
||||
"widgets_values": ["A"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.7"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -50,6 +50,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {}
|
||||
"groupNodes": {},
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -92,10 +92,14 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": false,
|
||||
"VHS_KeepIntermediate": false
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -106,10 +106,7 @@
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
|
||||
@@ -368,10 +368,10 @@
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
149.9747408641311,
|
||||
383.8593224280729
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
11
browser_tests/assets/widgets/load_animated_webp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"8": {
|
||||
"inputs": {
|
||||
"image": "animated_web.webp"
|
||||
},
|
||||
"class_type": "DevToolsLoadAnimatedImageTest",
|
||||
"_meta": {
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
64
browser_tests/assets/widgets/save_animated_webp.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"id": "3f1fcbf9-f9de-4935-8fad-401813f61b13",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveAnimatedWEBP",
|
||||
"pos": [336, 104],
|
||||
"size": [210, 368],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI", 6, true, 80, "default"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DevToolsLoadAnimatedImageTest",
|
||||
"pos": [64, 104],
|
||||
"size": [210, 316],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [4]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
|
||||
},
|
||||
"widgets_values": ["animated_web.webp", "image"]
|
||||
}
|
||||
],
|
||||
"links": [[4, 10, 0, 9, 0, "IMAGE"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
BIN
browser_tests/assets/workflow.m4v
Normal file
BIN
browser_tests/assets/workflow.mov
Normal file
BIN
browser_tests/assets/workflow.mp4
Normal file
596
browser_tests/assets/workflow.svg
Normal file
|
After Width: | Height: | Size: 75 KiB |
@@ -133,6 +133,9 @@ export class ComfyPage {
|
||||
// Inputs
|
||||
public readonly workflowUploadInput: Locator
|
||||
|
||||
// Toasts
|
||||
public readonly visibleToasts: Locator
|
||||
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly menu: ComfyMenu
|
||||
@@ -159,6 +162,8 @@ export class ComfyPage {
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.visibleToasts = page.locator('.p-toast-message:visible')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
@@ -396,6 +401,30 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async deleteWorkflow(
|
||||
workflowName: string,
|
||||
whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing'
|
||||
) {
|
||||
// Open workflows tab
|
||||
const { workflowsTab } = this.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
// Action to take if workflow missing
|
||||
if (whenMissing === 'ignoreMissing') {
|
||||
const workflows = await workflowsTab.getTopLevelSavedWorkflowNames()
|
||||
if (!workflows.includes(workflowName)) return
|
||||
}
|
||||
|
||||
// Delete workflow
|
||||
await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' })
|
||||
await this.clickContextMenuItem('Delete')
|
||||
await this.confirmDialog.delete.click()
|
||||
|
||||
// Clear toast & close tab
|
||||
await this.closeToasts(1)
|
||||
await workflowsTab.close()
|
||||
}
|
||||
|
||||
async resetView() {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
await this.resetViewButton.click()
|
||||
@@ -412,7 +441,20 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async getVisibleToastCount() {
|
||||
return await this.page.locator('.p-toast:visible').count()
|
||||
return await this.visibleToasts.count()
|
||||
}
|
||||
|
||||
async closeToasts(requireCount = 0) {
|
||||
if (requireCount) await expect(this.visibleToasts).toHaveCount(requireCount)
|
||||
|
||||
// Clear all toasts
|
||||
const toastCloseButtons = await this.page
|
||||
.locator('.p-toast-close-button')
|
||||
.all()
|
||||
for (const button of toastCloseButtons) {
|
||||
await button.click()
|
||||
}
|
||||
await expect(this.visibleToasts).toHaveCount(0)
|
||||
}
|
||||
|
||||
async clickTextEncodeNode1() {
|
||||
@@ -463,87 +505,129 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
async dragAndDropExternalResource(
|
||||
options: {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 } } = options
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
|
||||
const filePath = this.assetPath(fileName)
|
||||
if (!fileName && !url)
|
||||
throw new Error('Must provide either fileName or url')
|
||||
|
||||
// Read the file content
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
const evaluateParams: {
|
||||
dropPosition: Position
|
||||
fileName?: string
|
||||
fileType?: string
|
||||
buffer?: Uint8Array | number[]
|
||||
url?: string
|
||||
} = { dropPosition }
|
||||
|
||||
// Get file type
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
// Dropping a file from the filesystem
|
||||
if (fileName) {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.svg')) return 'image/svg+xml'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
evaluateParams.fileName = fileName
|
||||
evaluateParams.fileType = getFileType(fileName)
|
||||
evaluateParams.buffer = [...new Uint8Array(buffer)]
|
||||
}
|
||||
|
||||
const fileType = getFileType(fileName)
|
||||
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
||||
if (url) evaluateParams.url = url
|
||||
|
||||
await this.page.evaluate(
|
||||
async ({ buffer, fileName, fileType, dropPosition }) => {
|
||||
const file = new File([new Uint8Array(buffer)], fileName, {
|
||||
type: fileType
|
||||
})
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(file)
|
||||
// Execute the drag and drop in the browser
|
||||
await this.page.evaluate(async (params) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
dropPosition.x,
|
||||
dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
console.error('No element found at drop position:', dropPosition)
|
||||
return { success: false, error: 'No element at position' }
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: dropPosition.x,
|
||||
clientY: dropPosition.y
|
||||
}
|
||||
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(dragOverEvent)
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetInfo: {
|
||||
tagName: targetElement.tagName,
|
||||
id: targetElement.id,
|
||||
classList: Array.from(targetElement.classList)
|
||||
// Add file if provided
|
||||
if (params.buffer && params.fileName && params.fileType) {
|
||||
const file = new File(
|
||||
[new Uint8Array(params.buffer)],
|
||||
params.fileName,
|
||||
{
|
||||
type: params.fileType
|
||||
}
|
||||
)
|
||||
dataTransfer.items.add(file)
|
||||
}
|
||||
|
||||
// Add URL data if provided
|
||||
if (params.url) {
|
||||
dataTransfer.setData('text/uri-list', params.url)
|
||||
dataTransfer.setData('text/x-moz-url', params.url)
|
||||
}
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
params.dropPosition.x,
|
||||
params.dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
console.error('No element found at drop position:', params.dropPosition)
|
||||
return { success: false, error: 'No element at position' }
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: params.dropPosition.x,
|
||||
clientY: params.dropPosition.y
|
||||
}
|
||||
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(dragOverEvent)
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetInfo: {
|
||||
tagName: targetElement.tagName,
|
||||
id: targetElement.id,
|
||||
classList: Array.from(targetElement.classList)
|
||||
}
|
||||
},
|
||||
{ buffer: [...new Uint8Array(buffer)], fileName, fileType, dropPosition }
|
||||
)
|
||||
}
|
||||
}, evaluateParams)
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
options: { dropPosition?: Position } = {}
|
||||
) {
|
||||
return this.dragAndDropExternalResource({ fileName, ...options })
|
||||
}
|
||||
|
||||
async dragAndDropURL(url: string, options: { dropPosition?: Position } = {}) {
|
||||
return this.dragAndDropExternalResource({ url, ...options })
|
||||
}
|
||||
|
||||
async dragNode2() {
|
||||
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 })
|
||||
await this.nextFrame()
|
||||
@@ -678,7 +762,7 @@ export class ComfyPage {
|
||||
y: 625
|
||||
}
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -690,7 +774,7 @@ export class ComfyPage {
|
||||
},
|
||||
button: 'right'
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -882,10 +966,16 @@ export class ComfyPage {
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
|
||||
}, pos)
|
||||
}
|
||||
|
||||
/** Get number of DOM widgets on the canvas. */
|
||||
async getDOMWidgetCount() {
|
||||
return await this.page.locator('.dom-widget').count()
|
||||
}
|
||||
|
||||
async getNodeRefById(id: NodeId) {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodes() {
|
||||
async getNodes(): Promise<LGraphNode[]> {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
})
|
||||
@@ -956,6 +1046,8 @@ export class ComfyPage {
|
||||
}
|
||||
}
|
||||
|
||||
export const testComfySnapToGridGridSize = 50
|
||||
|
||||
export const comfyPageFixture = base.extend<{
|
||||
comfyPage: ComfyPage
|
||||
comfyMouse: ComfyMouse
|
||||
@@ -982,7 +1074,8 @@ export const comfyPageFixture = base.extend<{
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -81,7 +81,7 @@ export class NodeWidgetReference {
|
||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||
|
||||
const [x, y, w, h] = node.getBounding()
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
return window['app'].canvasPosToClientPos([
|
||||
x + w / 2,
|
||||
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
||||
])
|
||||
@@ -94,6 +94,36 @@ export class NodeWidgetReference {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The position of the widget's associated socket
|
||||
*/
|
||||
async getSocketPosition(): 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 slot = node.inputs.find(
|
||||
(slot) => slot.widget?.name === widget.name
|
||||
)
|
||||
if (!slot) throw new Error(`Socket ${widget.name} not found.`)
|
||||
|
||||
const [x, y] = node.getBounding()
|
||||
return window['app'].canvasPosToClientPos([
|
||||
x + slot.pos[0],
|
||||
y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT']
|
||||
])
|
||||
},
|
||||
[this.node.id, this.index] as const
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
|
||||
async click() {
|
||||
await this.node.comfyPage.canvas.click({
|
||||
position: await this.getPosition()
|
||||
@@ -250,7 +280,7 @@ export class NodeReference {
|
||||
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
||||
await this.comfyPage.dragAndDrop(
|
||||
await originSlot.getPosition(),
|
||||
await targetWidget.getPosition()
|
||||
await targetWidget.getSocketPosition()
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
|
||||
251
browser_tests/tests/backgroundImageUpload.spec.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Background Image Upload', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Reset the background image setting before each test
|
||||
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
// Clean up background image setting after each test
|
||||
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
|
||||
})
|
||||
|
||||
test('should show background image upload component in settings', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Navigate to Appearance category
|
||||
const appearanceOption = comfyPage.page.locator('text=Appearance')
|
||||
await appearanceOption.click()
|
||||
|
||||
// Find the background image setting
|
||||
const backgroundImageSetting = comfyPage.page.locator(
|
||||
'#Comfy\\.Canvas\\.BackgroundImage'
|
||||
)
|
||||
await expect(backgroundImageSetting).toBeVisible()
|
||||
|
||||
// Verify the component has the expected elements using semantic selectors
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
await expect(urlInput).toBeVisible()
|
||||
await expect(urlInput).toHaveAttribute('placeholder')
|
||||
|
||||
const uploadButton = backgroundImageSetting.locator(
|
||||
'button:has(.pi-upload)'
|
||||
)
|
||||
await expect(uploadButton).toBeVisible()
|
||||
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
await expect(clearButton).toBeVisible()
|
||||
await expect(clearButton).toBeDisabled() // Should be disabled when no image
|
||||
})
|
||||
|
||||
test('should upload image file and set as background', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Navigate to Appearance category
|
||||
const appearanceOption = comfyPage.page.locator('text=Appearance')
|
||||
await appearanceOption.click()
|
||||
|
||||
// Find the background image setting
|
||||
const backgroundImageSetting = comfyPage.page.locator(
|
||||
'#Comfy\\.Canvas\\.BackgroundImage'
|
||||
)
|
||||
// Click the upload button to trigger file input
|
||||
const uploadButton = backgroundImageSetting.locator(
|
||||
'button:has(.pi-upload)'
|
||||
)
|
||||
|
||||
// Set up file upload handler
|
||||
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
|
||||
await uploadButton.click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
|
||||
// Upload the test image
|
||||
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
|
||||
|
||||
// Wait for upload to complete and verify the setting was updated
|
||||
await comfyPage.page.waitForTimeout(500) // Give time for file reading
|
||||
|
||||
// Verify the URL input now has an API URL
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
const inputValue = await urlInput.inputValue()
|
||||
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||
|
||||
// Verify clear button is now enabled
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
await expect(clearButton).toBeEnabled()
|
||||
|
||||
// Verify the setting value was actually set
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Canvas.BackgroundImage'
|
||||
)
|
||||
expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||
})
|
||||
|
||||
test('should accept URL input for background image', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const testImageUrl = 'https://example.com/test-image.png'
|
||||
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Navigate to Appearance category
|
||||
const appearanceOption = comfyPage.page.locator('text=Appearance')
|
||||
await appearanceOption.click()
|
||||
|
||||
// Find the background image setting
|
||||
const backgroundImageSetting = comfyPage.page.locator(
|
||||
'#Comfy\\.Canvas\\.BackgroundImage'
|
||||
)
|
||||
// Enter URL in the input field
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
await urlInput.fill(testImageUrl)
|
||||
|
||||
// Trigger blur event to ensure the value is set
|
||||
await urlInput.blur()
|
||||
|
||||
// Verify clear button is now enabled
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
await expect(clearButton).toBeEnabled()
|
||||
|
||||
// Verify the setting value was updated
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Canvas.BackgroundImage'
|
||||
)
|
||||
expect(settingValue).toBe(testImageUrl)
|
||||
})
|
||||
|
||||
test('should clear background image when clear button is clicked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const testImageUrl = 'https://example.com/test-image.png'
|
||||
|
||||
// First set a background image
|
||||
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl)
|
||||
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Navigate to Appearance category
|
||||
const appearanceOption = comfyPage.page.locator('text=Appearance')
|
||||
await appearanceOption.click()
|
||||
|
||||
// Find the background image setting
|
||||
const backgroundImageSetting = comfyPage.page.locator(
|
||||
'#Comfy\\.Canvas\\.BackgroundImage'
|
||||
)
|
||||
// Verify the input has the test URL
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
await expect(urlInput).toHaveValue(testImageUrl)
|
||||
|
||||
// Verify clear button is enabled
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
await expect(clearButton).toBeEnabled()
|
||||
|
||||
// Click the clear button
|
||||
await clearButton.click()
|
||||
|
||||
// Verify the input is now empty
|
||||
await expect(urlInput).toHaveValue('')
|
||||
|
||||
// Verify clear button is now disabled
|
||||
await expect(clearButton).toBeDisabled()
|
||||
|
||||
// Verify the setting value was cleared
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Canvas.BackgroundImage'
|
||||
)
|
||||
expect(settingValue).toBe('')
|
||||
})
|
||||
|
||||
test('should show tooltip on upload and clear buttons', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Navigate to Appearance category
|
||||
const appearanceOption = comfyPage.page.locator('text=Appearance')
|
||||
await appearanceOption.click()
|
||||
|
||||
// Find the background image setting
|
||||
const backgroundImageSetting = comfyPage.page.locator(
|
||||
'#Comfy\\.Canvas\\.BackgroundImage'
|
||||
)
|
||||
// Hover over upload button and verify tooltip appears
|
||||
const uploadButton = backgroundImageSetting.locator(
|
||||
'button:has(.pi-upload)'
|
||||
)
|
||||
await uploadButton.hover()
|
||||
|
||||
// Wait for tooltip to appear and verify it exists
|
||||
await comfyPage.page.waitForTimeout(700) // Tooltip delay
|
||||
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
|
||||
await expect(uploadTooltip).toBeVisible()
|
||||
|
||||
// Move away to hide tooltip
|
||||
await comfyPage.page.locator('body').hover()
|
||||
await comfyPage.page.waitForTimeout(100)
|
||||
|
||||
// Set a background to enable clear button
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
await urlInput.fill('https://example.com/test.png')
|
||||
await urlInput.blur()
|
||||
|
||||
// Hover over clear button and verify tooltip appears
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
await clearButton.hover()
|
||||
|
||||
// Wait for tooltip to appear and verify it exists
|
||||
await comfyPage.page.waitForTimeout(700) // Tooltip delay
|
||||
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
|
||||
await expect(clearTooltip).toBeVisible()
|
||||
})
|
||||
|
||||
test('should maintain reactive updates between URL input and clear button state', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Navigate to Appearance category
|
||||
const appearanceOption = comfyPage.page.locator('text=Appearance')
|
||||
await appearanceOption.click()
|
||||
|
||||
// Find the background image setting
|
||||
const backgroundImageSetting = comfyPage.page.locator(
|
||||
'#Comfy\\.Canvas\\.BackgroundImage'
|
||||
)
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
|
||||
// Initially clear button should be disabled
|
||||
await expect(clearButton).toBeDisabled()
|
||||
|
||||
// Type some text - clear button should become enabled
|
||||
await urlInput.fill('test')
|
||||
await expect(clearButton).toBeEnabled()
|
||||
|
||||
// Clear the text manually - clear button should become disabled again
|
||||
await urlInput.fill('')
|
||||
await expect(clearButton).toBeDisabled()
|
||||
|
||||
// Add text again - clear button should become enabled
|
||||
await urlInput.fill('https://example.com/image.png')
|
||||
await expect(clearButton).toBeEnabled()
|
||||
|
||||
// Use clear button - should clear input and disable itself
|
||||
await clearButton.click()
|
||||
await expect(urlInput).toHaveValue('')
|
||||
await expect(clearButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
139
browser_tests/tests/chatHistory.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
interface ChatHistoryEntry {
|
||||
prompt: string
|
||||
response: string
|
||||
response_id: string
|
||||
}
|
||||
|
||||
async function renderChatHistory(page: Page, history: ChatHistoryEntry[]) {
|
||||
const nodeId = await page.evaluate(() => window['app'].graph.nodes[0]?.id)
|
||||
// Simulate API sending display_component message
|
||||
await page.evaluate(
|
||||
({ nodeId, history }) => {
|
||||
const event = new CustomEvent('display_component', {
|
||||
detail: {
|
||||
node_id: nodeId,
|
||||
component: 'ChatHistoryWidget',
|
||||
props: {
|
||||
history: JSON.stringify(history)
|
||||
}
|
||||
}
|
||||
})
|
||||
window['app'].api.dispatchEvent(event)
|
||||
return true
|
||||
},
|
||||
{ nodeId, history }
|
||||
)
|
||||
|
||||
return nodeId
|
||||
}
|
||||
|
||||
test.describe('Chat History Widget', () => {
|
||||
let nodeId: string
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
nodeId = await renderChatHistory(comfyPage.page, [
|
||||
{ prompt: 'Hello', response: 'World', response_id: '123' }
|
||||
])
|
||||
// Wait for chat history to be rendered
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
})
|
||||
|
||||
test('displays chat history when receiving display_component message', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Verify the chat history is displayed correctly
|
||||
await expect(comfyPage.page.getByText('Hello')).toBeVisible()
|
||||
await expect(comfyPage.page.getByText('World')).toBeVisible()
|
||||
})
|
||||
|
||||
test('handles message editing interaction', async ({ comfyPage }) => {
|
||||
// Get first node's ID
|
||||
nodeId = await comfyPage.page.evaluate(() => {
|
||||
const node = window['app'].graph.nodes[0]
|
||||
|
||||
// Make sure the node has a prompt widget (for editing functionality)
|
||||
if (!node.widgets) {
|
||||
node.widgets = []
|
||||
}
|
||||
|
||||
// Add a prompt widget if it doesn't exist
|
||||
if (!node.widgets.find((w) => w.name === 'prompt')) {
|
||||
node.widgets.push({
|
||||
name: 'prompt',
|
||||
type: 'text',
|
||||
value: 'Original prompt'
|
||||
})
|
||||
}
|
||||
|
||||
return node.id
|
||||
})
|
||||
|
||||
await renderChatHistory(comfyPage.page, [
|
||||
{
|
||||
prompt: 'Message 1',
|
||||
response: 'Response 1',
|
||||
response_id: '123'
|
||||
},
|
||||
{
|
||||
prompt: 'Message 2',
|
||||
response: 'Response 2',
|
||||
response_id: '456'
|
||||
}
|
||||
])
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
|
||||
const originalTextAreaInput = await comfyPage.page
|
||||
.getByPlaceholder('text')
|
||||
.nth(1)
|
||||
.inputValue()
|
||||
|
||||
// Click edit button on first message
|
||||
await comfyPage.page.getByLabel('Edit').first().click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify cancel button appears
|
||||
await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible()
|
||||
|
||||
// Click cancel edit
|
||||
await comfyPage.page.getByLabel('Cancel').click()
|
||||
|
||||
// Verify prompt input is restored
|
||||
await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue(
|
||||
originalTextAreaInput
|
||||
)
|
||||
})
|
||||
|
||||
test('handles real-time updates to chat history', async ({ comfyPage }) => {
|
||||
// Send initial history
|
||||
await renderChatHistory(comfyPage.page, [
|
||||
{
|
||||
prompt: 'Initial message',
|
||||
response: 'Initial response',
|
||||
response_id: '123'
|
||||
}
|
||||
])
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
|
||||
// Update history with additional messages
|
||||
await renderChatHistory(comfyPage.page, [
|
||||
{
|
||||
prompt: 'Follow-up',
|
||||
response: 'New response',
|
||||
response_id: '456'
|
||||
}
|
||||
])
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
|
||||
// Move mouse over the canvas to force update
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify new messages appear
|
||||
await expect(comfyPage.page.getByText('Follow-up')).toBeVisible()
|
||||
await expect(comfyPage.page.getByText('New response')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -48,6 +48,7 @@ const customColorPalettes: Record<string, Palette> = {
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
|
||||
WIDGET_DISABLED_TEXT_COLOR: '#646464',
|
||||
LINK_COLOR: '#9A9',
|
||||
EVENT_LINK_COLOR: '#A86',
|
||||
CONNECTING_LINK_COLOR: '#AFA'
|
||||
@@ -111,6 +112,7 @@ const customColorPalettes: Record<string, Palette> = {
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
|
||||
WIDGET_DISABLED_TEXT_COLOR: '#646464',
|
||||
LINK_COLOR: '#9A9',
|
||||
EVENT_LINK_COLOR: '#A86',
|
||||
CONNECTING_LINK_COLOR: '#AFA'
|
||||
|
||||
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
@@ -103,7 +103,7 @@ test.describe('Missing models warning', () => {
|
||||
}
|
||||
])
|
||||
}
|
||||
comfyPage.page.route(
|
||||
await comfyPage.page.route(
|
||||
'**/api/experiment/models',
|
||||
(route) => route.fulfill(modelFoldersRes),
|
||||
{ times: 1 }
|
||||
@@ -121,7 +121,7 @@ test.describe('Missing models warning', () => {
|
||||
}
|
||||
])
|
||||
}
|
||||
comfyPage.page.route(
|
||||
await comfyPage.page.route(
|
||||
'**/api/experiment/models/text_encoders',
|
||||
(route) => route.fulfill(clipModelsRes),
|
||||
{ times: 1 }
|
||||
@@ -133,6 +133,18 @@ test.describe('Missing models warning', () => {
|
||||
await expect(missingModelsWarning).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Should not display warning when model metadata exists but widget values have changed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// This tests the scenario where outdated model metadata exists in the workflow
|
||||
// but the actual selected models (widget values) have changed
|
||||
await comfyPage.loadWorkflow('model_metadata_widget_mismatch')
|
||||
|
||||
// The missing models warning should NOT appear
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).not.toBeVisible()
|
||||
})
|
||||
|
||||
// Flaky test after parallelization
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
|
||||
test.skip('Should download missing model when clicking download button', async ({
|
||||
@@ -341,3 +353,30 @@ test.describe('Error dialog', () => {
|
||||
await expect(errorDialog).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Signin dialog', () => {
|
||||
test('Paste content to signin dialog should not paste node on canvas', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeNum = (await comfyPage.getNodes()).length
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.ctrlC()
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.fill('test_password')
|
||||
await textBox.press('Control+a')
|
||||
await textBox.press('Control+c')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog.showSignInDialog()
|
||||
})
|
||||
|
||||
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
|
||||
await input.waitFor({ state: 'visible' })
|
||||
await input.press('Control+v')
|
||||
await expect(input).toHaveValue('test_password')
|
||||
|
||||
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,4 +24,27 @@ test.describe('DOM Widget', () => {
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Position update when entering focus mode', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
|
||||
})
|
||||
|
||||
// No DOM widget should be created by creation of interim LGraphNode objects.
|
||||
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.getDOMWidgetCount()
|
||||
|
||||
// TextEncodeNode1
|
||||
await comfyPage.page.mouse.move(618, 191)
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
|
||||
const finalCount = await comfyPage.getDOMWidgetCount()
|
||||
expect(finalCount).toBe(initialCount + 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 83 KiB |
@@ -18,3 +18,27 @@ test.describe('Execution', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Execute to selected output nodes', () => {
|
||||
test('Execute to selected output nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/partial_execution')
|
||||
const input = await comfyPage.getNodeRefById(3)
|
||||
const output1 = await comfyPage.getNodeRefById(1)
|
||||
const output2 = await comfyPage.getNodeRefById(4)
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
// @note: Wait for the execution to finish. We might want to move to a more
|
||||
// reliable way to wait for the execution to finish. Workflow in this test
|
||||
// is simple enough that this is fine for now.
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
21
browser_tests/tests/graph.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Graph', () => {
|
||||
// Should be able to fix link input slot index after swap the input order
|
||||
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
|
||||
test('Fix link input slots', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('input_order_swap')
|
||||
expect(
|
||||
await comfyPage.page.evaluate(() => {
|
||||
return window['app'].graph.links.get(1)?.target_slot
|
||||
})
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
test('Validate workflow links', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('bad_link')
|
||||
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 98 KiB |
@@ -1,6 +1,12 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { Position } from '@vueuse/core'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -57,8 +63,10 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
||||
})
|
||||
|
||||
test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
const dragSelectNodes = async (
|
||||
comfyPage: ComfyPage,
|
||||
clipNodes: NodeReference[]
|
||||
) => {
|
||||
const clipNode1Pos = await clipNodes[0].getPosition()
|
||||
const clipNode2Pos = await clipNodes[1].getPosition()
|
||||
const offset = 64
|
||||
@@ -74,10 +82,67 @@ test.describe('Node Interaction', () => {
|
||||
}
|
||||
)
|
||||
await comfyPage.page.keyboard.up('Meta')
|
||||
}
|
||||
|
||||
test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
await dragSelectNodes(comfyPage, clipNodes)
|
||||
expect(await comfyPage.getSelectedGraphNodesCount()).toBe(
|
||||
clipNodes.length
|
||||
)
|
||||
})
|
||||
|
||||
test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
const getPositions = () =>
|
||||
Promise.all(clipNodes.map((node) => node.getPosition()))
|
||||
const testDirection = async ({
|
||||
direction,
|
||||
expectedPosition
|
||||
}: {
|
||||
direction: string
|
||||
expectedPosition: (originalPosition: Position) => Position
|
||||
}) => {
|
||||
const originalPositions = await getPositions()
|
||||
await dragSelectNodes(comfyPage, clipNodes)
|
||||
await comfyPage.executeCommand(
|
||||
`Comfy.Canvas.MoveSelectedNodes.${direction}`
|
||||
)
|
||||
await comfyPage.canvas.press(`Control+Arrow${direction}`)
|
||||
const newPositions = await getPositions()
|
||||
expect(newPositions).toEqual(originalPositions.map(expectedPosition))
|
||||
}
|
||||
await testDirection({
|
||||
direction: 'Down',
|
||||
expectedPosition: (originalPosition) => ({
|
||||
...originalPosition,
|
||||
y: originalPosition.y + testComfySnapToGridGridSize
|
||||
})
|
||||
})
|
||||
await testDirection({
|
||||
direction: 'Right',
|
||||
expectedPosition: (originalPosition) => ({
|
||||
...originalPosition,
|
||||
x: originalPosition.x + testComfySnapToGridGridSize
|
||||
})
|
||||
})
|
||||
await testDirection({
|
||||
direction: 'Up',
|
||||
expectedPosition: (originalPosition) => ({
|
||||
...originalPosition,
|
||||
y: originalPosition.y - testComfySnapToGridGridSize
|
||||
})
|
||||
})
|
||||
await testDirection({
|
||||
direction: 'Left',
|
||||
expectedPosition: (originalPosition) => ({
|
||||
...originalPosition,
|
||||
x: originalPosition.x - testComfySnapToGridGridSize
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Can drag node', async ({ comfyPage }) => {
|
||||
@@ -666,6 +731,12 @@ test.describe('Load workflow', () => {
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
|
||||
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
@@ -683,3 +754,42 @@ test.describe('Load duplicate workflow', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Viewport settings', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')
|
||||
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Keeps viewport settings when changing tabs', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
// Screenshot the canvas element
|
||||
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png')
|
||||
|
||||
// Save workflow as a new file, then zoom out before screen shot
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
||||
await comfyMouse.move(comfyPage.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
}
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png')
|
||||
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
|
||||
// Go back to Workflow A
|
||||
await tabA.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png')
|
||||
|
||||
// And back to Workflow B
|
||||
await tabB.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 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: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |