Compare commits
400 Commits
core/1.15
...
queue_text
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33dc74fa2a | ||
|
|
bd37da7161 | ||
|
|
0b8d98e1e7 | ||
|
|
25a6b9f393 | ||
|
|
bdf32790c9 | ||
|
|
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 |
11
.cursorrules
@@ -8,6 +8,15 @@ const vue3CompositionApiBestPractices = [
|
||||
"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:
|
||||
|
||||
const { nodes, showTotal = true } = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
}>()
|
||||
|
||||
",
|
||||
"Organize vue component in <template> <script> <style> order",
|
||||
]
|
||||
|
||||
// Folder structure
|
||||
@@ -40,4 +49,6 @@ 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.
|
||||
`;
|
||||
|
||||
12
.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,14 @@ 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
|
||||
|
||||
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@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
packages-dir: comfyui_frontend_package/dist
|
||||
2
.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
|
||||
|
||||
3
.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
|
||||
|
||||
|
||||
2
.github/workflows/update-electron-types.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
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: |
|
||||
|
||||
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.
|
||||
`
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
38
CLAUDE.md
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
- 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 speicifc 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 refrence 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.
|
||||
166
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,12 @@ 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.
|
||||
|
||||
### Unit Test
|
||||
|
||||
- `npm i` to install all dependencies
|
||||
@@ -586,3 +638,103 @@ 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
|
||||
|
||||
> **Note**: For comprehensive troubleshooting and how-to guides, please refer to our [official documentation](https://docs.comfy.org/). This section covers only the most common issues related to frontend development.
|
||||
|
||||
> **Desktop Users**: For issues specific to the desktop application, please refer to the [ComfyUI desktop repository](https://github.com/Comfy-Org/desktop).
|
||||
|
||||
### Debugging Custom Node (Extension) Issues
|
||||
|
||||
If you're experiencing crashes, errors, or unexpected behavior with ComfyUI, it's often caused by custom nodes (extensions). Follow these steps to identify and resolve the issues:
|
||||
|
||||
#### Step 1: Verify if custom nodes are causing the problem
|
||||
|
||||
Run ComfyUI with the `--disable-all-custom-nodes` flag:
|
||||
|
||||
```bash
|
||||
python main.py --disable-all-custom-nodes
|
||||
```
|
||||
|
||||
If the issue disappears, a custom node is the culprit. Proceed to the next step.
|
||||
|
||||
#### Step 2: Identify the problematic custom node using binary search
|
||||
|
||||
Rather than disabling nodes one by one, use this more efficient approach:
|
||||
|
||||
1. Temporarily move half of your custom nodes out of the `custom_nodes` directory
|
||||
```bash
|
||||
# Create a temporary directory
|
||||
# Linux/Mac
|
||||
mkdir ~/custom_nodes_disabled
|
||||
|
||||
# Windows
|
||||
mkdir %USERPROFILE%\custom_nodes_disabled
|
||||
|
||||
# Move half of your custom nodes (assuming you have node1 through node8)
|
||||
# Linux/Mac
|
||||
mv custom_nodes/node1 custom_nodes/node2 custom_nodes/node3 custom_nodes/node4 ~/custom_nodes_disabled/
|
||||
|
||||
# Windows
|
||||
move custom_nodes\node1 custom_nodes\node2 custom_nodes\node3 custom_nodes\node4 %USERPROFILE%\custom_nodes_disabled\
|
||||
```
|
||||
|
||||
2. Run ComfyUI again
|
||||
- If the issue persists: The problem is in nodes 5-8 (the remaining half)
|
||||
- If the issue disappears: The problem is in nodes 1-4 (the moved half)
|
||||
|
||||
3. Let's assume the issue disappeared, so the problem is in nodes 1-4. Move half of these for the next test:
|
||||
```bash
|
||||
# Move nodes 3-4 back to custom_nodes
|
||||
# Linux/Mac
|
||||
mv ~/custom_nodes_disabled/node3 ~/custom_nodes_disabled/node4 custom_nodes/
|
||||
|
||||
# Windows
|
||||
move %USERPROFILE%\custom_nodes_disabled\node3 %USERPROFILE%\custom_nodes_disabled\node4 custom_nodes\
|
||||
```
|
||||
|
||||
4. Run ComfyUI again
|
||||
- If the issue reappears: The problem is in nodes 3-4
|
||||
- If issue still gone: The problem is in nodes 1-2
|
||||
|
||||
5. Let's assume the issue reappeared, so the problem is in nodes 3-4. Test each one:
|
||||
```bash
|
||||
# Move node 3 back to disabled
|
||||
# Linux/Mac
|
||||
mv custom_nodes/node3 ~/custom_nodes_disabled/
|
||||
|
||||
# Windows
|
||||
move custom_nodes\node3 %USERPROFILE%\custom_nodes_disabled\
|
||||
```
|
||||
|
||||
6. Run ComfyUI again
|
||||
- If the issue disappears: node3 is the problem
|
||||
- If issue persists: node4 is the problem
|
||||
|
||||
7. Repeat until you identify the specific problematic node
|
||||
|
||||
#### Step 3: Update or replace the problematic node
|
||||
|
||||
Once identified:
|
||||
1. Check for updates to the problematic custom node
|
||||
2. Consider alternatives with similar functionality
|
||||
3. Report the issue to the custom node developer with specific details
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
- **"Module not found" errors**: Usually indicates missing Python dependencies. Check the custom node's `requirements.txt` file for required packages and install them:
|
||||
```bash
|
||||
pip install -r custom_nodes/problematic_node/requirements.txt
|
||||
```
|
||||
|
||||
- **Frontend or Templates Package Not Updated**: After updating ComfyUI via Git, ensure you update the frontend dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- **Can't Find Custom Node**: Make sure to disable node validation in ComfyUI settings.
|
||||
|
||||
- **Error Toast About Workflow Failing Validation**: Report the issue to the ComfyUI team. As a temporary workaround, disable workflow validation in settings.
|
||||
|
||||
- **Login Issues When Not on Localhost**: Normal login is only available when accessing from localhost. If you're running ComfyUI via LAN, another domain, or headless, you can use our API key feature to authenticate. The API key lets you log in normally through the UI. Generate an API key at [platform.comfy.org/login](https://platform.comfy.org/login) and use it in the API Key field in the login dialog or with the `--api-key` command line argument. Refer to our [API Key Integration Guide](https://docs.comfy.org/essentials/comfyui-server/api-key-integration#integration-of-api-key-to-use-comfyui-api-nodes) for complete setup instructions.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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 |
@@ -341,3 +341,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: 85 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -666,6 +666,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 +689,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 |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |