Compare commits

..

3 Commits

Author SHA1 Message Date
filtered
628b44051b nit - fix TS type 2025-01-22 08:26:22 +11:00
filtered
a7a5e3cf67 [Refactor] Task state updates to TaskRunner 2025-01-22 08:18:42 +11:00
filtered
64e218a9f3 [Refactor] Task execution into task runner class 2025-01-22 08:18:42 +11:00
904 changed files with 19522 additions and 51193 deletions

View File

@@ -15,7 +15,7 @@ const folderStructure = `
src/
components/
constants/
composables/
hooks/
views/
stores/
services/

View File

@@ -11,9 +11,18 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# and public addresses.
VITE_REMOTE_DEV=false
# The target ComfyUI checkout directory to deploy the frontend code to.
# The dist directory will be copied to {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev
# Add `--front-end-root {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev`
# to ComfyUI launch script to serve the custom web version.
DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
# The directory containing the ComfyUI installation used to run Playwright tests.
# If you aren't using a separate install for testing, point this to your regular install.
TEST_COMFYUI_DIR=/home/ComfyUI
# The directory containing the ComfyUI_examples repo used to extract test workflows.
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true

View File

@@ -16,18 +16,7 @@ body:
- type: textarea
attributes:
label: Frontend Version
description: |
What is the frontend version you are using? You can check this in the settings dialog.
<details>
<summary>Click to show where to find the version</summary>
Open the setting by clicking the cog icon in the bottom-left of the screen, then click `About`.
![Frontend version](https://github.com/user-attachments/assets/561fb7c3-3012-457c-a494-9bdc1ff035c0)
</details>
description: 'What is the frontend version you are using? You can check this in the settings dialog'
validations:
required: true
- type: textarea
@@ -83,8 +72,7 @@ body:
- Other
- type: textarea
attributes:
label: Other Information
description: 'Any other context, details, or screenshots that might help solve the issue.'
placeholder: 'Add any other relevant information here...'
label: Other
description: 'Any other additional information you think might be helpful.'
validations:
required: false

View File

@@ -2,7 +2,10 @@ name: ESLint
on:
pull_request:
branches: [ main, master, dev*, core/*, desktop/* ]
branches:
- main
- master
- 'dev*'
jobs:
eslint:
@@ -10,8 +13,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
node-version: lts/*
- run: npm ci
- run: npm run lint
- run: npm run lint

View File

@@ -2,7 +2,7 @@ name: Prettier Check
on:
pull_request:
branches: [ main, master, dev*, core/*, desktop/* ]
branches: [ main, master, dev* ]
jobs:
prettier:
@@ -12,12 +12,12 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Run Prettier check
run: npm run format:check
run: npm run format:check

View File

@@ -1,5 +1,4 @@
name: Update Locales for given custom node repository
on:
workflow_dispatch:
inputs:
@@ -24,27 +23,27 @@ jobs:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: comfyanonymous/ComfyUI
path: ComfyUI
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI_devtools
path: ComfyUI/custom_nodes/ComfyUI_devtools
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
- name: Checkout custom node repository
uses: actions/checkout@v4
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
node-version: lts/*
- uses: actions/setup-python@v4
with:
python-version: '3.10'
@@ -54,12 +53,14 @@ jobs:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
shell: bash
working-directory: ComfyUI
- name: Install custom node requirements
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
shell: bash
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- name: Build & Install ComfyUI_frontend
run: |
@@ -67,12 +68,14 @@ jobs:
npm run build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
shell: bash
working-directory: ComfyUI_frontend
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
shell: bash
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
@@ -150,7 +153,7 @@ jobs:
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Create PR
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli

View File

@@ -13,7 +13,7 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -1,5 +1,4 @@
name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]
@@ -10,7 +9,7 @@ jobs:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
@@ -34,11 +33,7 @@ jobs:
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git push origin HEAD:${{ github.head_ref }}

View File

@@ -2,56 +2,35 @@ name: Create Release Draft
on:
pull_request:
types: [ closed ]
branches: [ main, core/* ]
types: [closed]
branches:
- main
- master
paths:
- 'package.json'
- "package.json"
jobs:
build:
draft_release:
runs-on: ubuntu-latest
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'Release')
outputs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
node-version: lts/*
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
run: |
npm ci
npm run fetch-templates
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist/
dist.zip
draft_release:
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: Create release
id: create_release
uses: softprops/action-gh-release@v2
@@ -60,57 +39,25 @@ jobs:
with:
files: |
dist.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' }}
draft: ${{ github.event.pull_request.base.ref != 'main' }}
tag_name: v${{ steps.current_version.outputs.version }}
draft: true
prerelease: false
generate_release_notes: true
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: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
make_latest: "true"
publish_types:
needs: build
runs-on: ubuntu-latest
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'Release')
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
registry-url: https://registry.npmjs.org
node-version: lts/*
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build:types
- name: Publish package
run: npm publish --access public
working-directory: dist
working-directory: ./dist
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,4 +1,5 @@
# Setting test expectation screenshots for Playwright
name: Update Playwright Expectations
on:
@@ -10,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -2,104 +2,73 @@ name: Tests CI
on:
push:
branches: [main, master, core/*, desktop/*]
branches:
- main
- master
pull_request:
branches: [main, master, dev*, core/*, desktop/*]
branches:
- main
- master
- 'dev*'
jobs:
setup:
jest-tests:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
ref: master
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Run Jest tests
run: |
npm run test:generate
npm run test:jest -- --verbose
working-directory: ComfyUI_frontend
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be'
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Build ComfyUI_frontend
run: |
npm ci
npm run fetch-templates
npm run build
working-directory: ComfyUI_frontend
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Cache setup
uses: actions/cache@v3
with:
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
playwright-tests:
needs: setup
playwright-tests-chromium:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, chromium-2x, mobile-chrome]
steps:
- name: Restore cached setup
uses: actions/cache@v3
with:
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests (Chromium)
run: npx playwright test --project=chromium
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-chromium
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- uses: actions/setup-python@v4
with:
python-version: '3.10'
playwright-tests-chromium-2x:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests (Chromium 2x)
run: npx playwright test --project=chromium-2x
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-chromium-2x
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests (${{ matrix.browser }})
run: npx playwright test --project=${{ matrix.browser }}
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.browser }}
path: ComfyUI_frontend/playwright-report/
retention-days: 30
playwright-tests-mobile-chrome:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests (Mobile Chrome)
run: npx playwright test --project=mobile-chrome
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-mobile-chrome
path: ComfyUI_frontend/playwright-report/
retention-days: 30

View File

@@ -1,44 +0,0 @@
name: Update Electron Types
on:
workflow_dispatch:
jobs:
update-electron-types:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
- name: Update electron types
run: npm install @comfyorg/comfyui-electron-types@latest
- name: Get new version
id: get-version
run: |
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package-lock.json')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_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: |
Automated update of desktop API types to version ${{ steps.get-version.outputs.NEW_VERSION }}.
branch: update-electron-types-${{ steps.get-version.outputs.NEW_VERSION }}
base: main
labels: |
dependencies
Electron

View File

@@ -1,43 +0,0 @@
name: Update Litegraph Dependency
on:
workflow_dispatch:
jobs:
update-litegraph:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Update litegraph
run: npm install @comfyorg/litegraph@latest
- name: Get new version
id: get-version
run: |
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package-lock.json')).packages['node_modules/@comfyorg/litegraph'].version)")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
title: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
body: |
Automated update of litegraph to version ${{ steps.get-version.outputs.NEW_VERSION }}.
Ref: https://github.com/Comfy-Org/litegraph.js/releases/tag/v${{ steps.get-version.outputs.NEW_VERSION }}
branch: update-litegraph-${{ steps.get-version.outputs.NEW_VERSION }}
base: main
labels: |
dependencies

View File

@@ -1,97 +0,0 @@
name: Update Comfy Registry API Types
on:
# Manual trigger
workflow_dispatch:
# Triggered from comfy-api repo
repository_dispatch:
types: [comfy-api-updated]
jobs:
update-registry-types:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Checkout comfy-api repository
uses: actions/checkout@v4
with:
repository: Comfy-Org/comfy-api
path: comfy-api
token: ${{ secrets.COMFY_API_PAT }}
clean: true
- name: Get API commit information
id: api-info
run: |
cd comfy-api
API_COMMIT=$(git rev-parse --short HEAD)
echo "commit=${API_COMMIT}" >> $GITHUB_OUTPUT
cd ..
- name: Generate API types
run: |
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
npx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
- name: Validate generated types
run: |
if [ ! -f ./src/types/comfyRegistryTypes.ts ]; then
echo "Error: Types file was not generated."
exit 1
fi
# Check if file is not empty
if [ ! -s ./src/types/comfyRegistryTypes.ts ]; then
echo "Error: Generated types file is empty."
exit 1
fi
- name: Check for changes
id: check-changes
run: |
if [[ -z $(git status --porcelain ./src/types/comfyRegistryTypes.ts) ]]; then
echo "No changes to Comfy Registry API types detected."
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
else
echo "Changes detected in Comfy Registry API types."
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'
title: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'
body: |
## Automated API Type Update
This PR updates the Comfy Registry API types from the latest comfy-api OpenAPI specification.
- API commit: ${{ steps.api-info.outputs.commit }}
- Generated on: ${{ github.event.repository.updated_at }}
These types are automatically generated using openapi-typescript.
branch: update-registry-types-${{ steps.api-info.outputs.commit }}
base: main
labels: CNR
delete-branch: true
add-paths: |
src/types/comfyRegistryTypes.ts

View File

@@ -1,51 +0,0 @@
name: Version Bump
on:
workflow_dispatch:
inputs:
version_type:
description: 'Version increment type'
required: true
default: 'patch'
type: 'choice'
options:
- patch
- minor
- major
jobs:
bump-version:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
- name: Bump version
id: bump-version
run: |
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Bump version to ${{ steps.bump-version.outputs.NEW_VERSION }}'
title: '${{ steps.bump-version.outputs.NEW_VERSION }}'
body: |
Automated version bump to ${{ steps.bump-version.outputs.NEW_VERSION }}
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
labels: |
Release

View File

@@ -2,9 +2,15 @@ name: Vitest Tests
on:
push:
branches: [ main, master, dev*, core/*, desktop/* ]
branches:
- main
- master
- 'dev*'
pull_request:
branches: [ main, master, dev*, core/*, desktop/* ]
branches:
- main
- master
- 'dev*'
jobs:
test:
@@ -14,14 +20,12 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Run Vitest tests
run: |
npm run test:component
npm run test:unit
run: npm run test:component

10
.gitignore vendored
View File

@@ -18,7 +18,6 @@ dist-ssr
!.vscode/extensions.json
!.vscode/tailwind.json
!.vscode/settings.json.default
!.vscode/launch.json.default
.idea
.DS_Store
*.suo
@@ -38,7 +37,7 @@ tests-ui/workflows/examples
/playwright-report/
/blob-report/
/playwright/.cache/
browser_tests/**/*-win32.png
browser_tests/*/*-win32.png
.env
@@ -48,10 +47,3 @@ dist.zip
# Generated JSON Schemas
/schemas/
# Workflow templates assets
# Hosted on https://github.com/Comfy-Org/workflow_templates
/public/templates/
# Temporary repository directory
templates_repo/

View File

@@ -9,8 +9,8 @@ module.exports = defineConfig({
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.
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
`

View File

@@ -1,16 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome on frontend dev",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true,
}
]
}

158
README.md
View File

@@ -31,22 +31,8 @@
## Release Schedule
The project follows a structured release process for each minor version, consisting of three distinct phases:
### Nightly Release
1. **Development Phase** - 1 week
- Active development of new features
- Code changes merged to the development branch
2. **Feature Freeze** - 1 week
- No new features accepted
- Only bug fixes are cherry-picked to the release branch
- Testing and stabilization of the codebase
3. **Publication**
- Release is published at the end of the freeze period
- Version is finalized and made available to all users
### Nightly Releases
Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
To use the latest nightly release, add the following command line argument to your ComfyUI launch script:
@@ -55,17 +41,18 @@ To use the latest nightly release, add the following command line argument to yo
--front-end-version Comfy-Org/ComfyUI_frontend@latest
```
## Overlapping Release Cycles
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously.
#### For Windows Stand-alone Build Users
### Example Release Cycle
Edit your `run_cpu.bat` or `run_nvidia_gpu.bat` file as follows:
| Week | Date Range | Version 1.1 | Version 1.2 | Version 1.3 | Patch Releases |
|------|------------|-------------|-------------|-------------|----------------|
| 1 | Mar 1-7 | Development | - | - | - |
| 2 | Mar 8-14 | Feature Freeze | Development | - | 1.1.0 through 1.1.6 (daily) |
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
```bat
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --front-end-version Comfy-Org/ComfyUI_frontend@latest
pause
```
### Stable Release
Stable releases are published bi-weekly in the ComfyUI main repository.
## Release Summary
@@ -481,35 +468,6 @@ We will support custom icons later.
![image](https://github.com/user-attachments/assets/7bff028a-bf91-4cab-bf97-55c243b3f5e0)
</details>
<details id='extension-api-selection-toolbox'>
<summary>v1.10.9: Selection Toolbox API</summary>
Extensions can register commands that appear in the selection toolbox when specific items are selected on the canvas.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'test.selection.command',
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
// Command logic here
}
}
],
// Return an array of command IDs to show in the selection toolbox
// when an item is selected
getSelectionToolboxCommands: (selectedItem) => ['test.selection.command']
})
```
The selection toolbox will display the command button when items are selected:
![Image](https://github.com/user-attachments/assets/28d91267-c0a9-4bd5-a7c4-36e8ec44c9bd)
</details>
## Development
### Tech Stack
@@ -517,7 +475,7 @@ The selection toolbox will display the command button when items are selected:
- [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
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
@@ -559,8 +517,10 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
### Unit Test
- `git clone https://github.com/comfyanonymous/ComfyUI_examples.git` to `tests-ui/ComfyUI_examples` or the EXAMPLE_REPO_PATH location specified in .env
- `npm i` to install all dependencies
- `npm run test:unit` to execute all unit tests.
- `npm run test:generate` to fetch `tests-ui/data/object_info.json`
- `npm run test:jest` to execute all unit tests.
### Component Test
@@ -572,17 +532,97 @@ Component test verifies Vue components in `src/components/`.
Playwright test verifies the whole app. See <https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/README.md> for details.
### litegraph.js
### LiteGraph
This repo is using litegraph package hosted on <https://github.com/Comfy-Org/litegraph.js>. Any changes to litegraph should be submitted in that repo instead.
#### Test litegraph.js changes
### Test litegraph changes
- Run `npm link` in the local litegraph repo.
- Run `npm link @comfyorg/litegraph` in this repo.
This will replace the litegraph package in this repo with the local litegraph repo.
### i18n
## Internationalization (i18n)
See [locales/README.md](src/locales/README.md) for details.
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
### Supported Languages
- en (English)
- zh (中文)
- ru (Русский)
- ja (日本語)
- ko (한국어)
- fr (Français)
### How to Add a New Language
We welcome the addition of new languages. You can add a new language by following these steps:
#### 1. Generate language files
We use [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/blob/master/packages/lobe-i18n/README.md) as our translation tool, which integrates with LLM for efficient localization.
Update the configuration file to include the new language(s) you wish to add:
```javascript
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'src/locales/en.json', // Base language file
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja'], // Add the new language(s) here
});
```
Set your OpenAI API Key by running the following command:
```sh
npx lobe-i18n --option
```
Once configured, generate the translation files with:
```sh
npx lobe-i18n locale
```
This will create the language files for the specified languages in the configuration.
#### 2. Update i18n Configuration
Import the newly generated locale file(s) in the `src/i18n.ts` file to include them in the application's i18n setup.
#### 3. Enable Selection of the New Language
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
```typescript
{
id: 'Comfy.Locale',
name: 'Locale',
type: 'combo',
// Add the new language(s) here
options: [
{ value: 'en', text: 'English' },
{ value: 'zh', text: '中文' },
{ value: 'ru', text: 'Русский' },
{ value: 'ja', text: '日本語' }
],
defaultValue: navigator.language.split('-')[0] || 'en'
},
```
This will make the new language selectable in the application's settings.
#### 4. Test the Translations
Start the development server, switch to the new language, and verify the translations.
You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
## Deploy
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
- Option 2: Copy everything under `dist/` to `ComfyUI/web/` in your ComfyUI checkout manually.

8
babel.config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"babel-plugin-transform-import-meta"
]
}

View File

@@ -1,9 +1,9 @@
import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test'
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
import { webSocketFixture } from '../fixtures/ws.ts'
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
import { comfyPageFixture } from './fixtures/ComfyPage'
import { webSocketFixture } from './fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture)

View File

@@ -114,7 +114,7 @@
{ "name": "VAE", "type": "VAE", "links": [8], "slot_index": 2 }
],
"properties": {},
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
"widgets_values": ["v1-5-pruned-emaonly.ckpt"]
}
],
"links": [

View File

@@ -1,163 +0,0 @@
{
"last_node_id": 19,
"last_link_id": 14,
"nodes": [
{
"id": 19,
"type": "workflow>two_VAE_decode",
"pos": [
1368.800048828125,
768.7999877929688
],
"size": [
418.1999816894531,
86
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "VAEDecode IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {}
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
},
"node_versions": {},
"ue_links": [],
"groupNodes": {
"two_VAE_decode": {
"nodes": [
{
"id": -1,
"type": "VAEDecode",
"pos": [
1368.800048828125,
768.7999877929688
],
"size": [
210,
46
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null,
"localized_name": "samples"
},
{
"name": "vae",
"type": "VAE",
"link": null,
"localized_name": "vae"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"localized_name": "IMAGE"
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"index": 0
},
{
"id": -1,
"type": "VAEDecode",
"pos": [
1368.800048828125,
873.7999877929688
],
"size": [
210,
46
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null,
"localized_name": "samples"
},
{
"name": "vae",
"type": "VAE",
"link": null,
"localized_name": "vae"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"localized_name": "IMAGE"
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"index": 1
}
],
"links": [],
"external": [],
"config": {
"1": {
"input": {
"samples": {
"visible": false
},
"vae": {
"visible": false
}
}
}
}
}
}
},
"version": 0.4
}

View File

@@ -18,7 +18,7 @@
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "text_encoders"
"directory": "clip"
}
],
"version": 0.4

View File

@@ -1,49 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "CheckpointLoaderSimple",
"pos": [256, 256],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": null
},
{
"name": "CLIP",
"type": "CLIP",
"links": null
},
{
"name": "VAE",
"type": "VAE",
"links": null
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"models": [
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "text_encoders"
}
]
},
"widgets_values": ["fake_model.safetensors"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -1,63 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "UNKNOWN NODE",
"pos": [
48,
86
],
"size": {
"0": 358.80780029296875,
"1": 314.7989501953125
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null,
"slot_index": 0
},
{
"name": "foo",
"type": "STRING",
"link": null,
"slot_index": 1,
"widget": {
"name": "foo"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [],
"slot_index": 0,
"shape": 6
}
],
"properties": {
"Node name for S&R": "UNKNOWN NODE"
},
"widgets_values": [
"wd-v1-4-moat-tagger-v2",
0.35,
0.85,
false,
false,
""
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -1,55 +0,0 @@
{
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "Note",
"pos": [
50, 50
],
"size": [
322.3645935058594,
167.91612243652344
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"Foo\n123"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 2,
"type": "MarkdownNote",
"pos": [
50, 300
],
"size": [
320.9985656738281,
179.52735900878906
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"# Bar\n123"
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -1,37 +0,0 @@
{
"last_node_id": 16,
"last_link_id": 17,
"nodes": [
{
"id": 16,
"type": "DevToolsNodeWithOptionalComboInput",
"pos": [1605, 480],
"size": [378, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsNodeWithOptionalComboInput"
},
"widgets_values": ["Random Unique Option 1740551583.3507228"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 2.0875710456451313,
"offset": [-1311.5753953400676, -176.7620403697558]
}
},
"version": 0.4
}

View File

@@ -1,59 +0,0 @@
{
"last_node_id": 3,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "PrimitiveNode",
"pos": [14, 43],
"size": [203.1999969482422, 40.36840057373047],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "connect to widget input",
"type": "*",
"links": [],
"slot_index": 0
}
],
"properties": {
"Run widget replace on values": false
}
},
{
"id": 2,
"type": "CLIPTextEncode",
"pos": [306.2463684082031, 45.30042266845703],
"size": [400, 200],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": null
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [""]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -1,104 +0,0 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 304.3653259277344,
"1": 42.15586471557617
},
"size": [
315,
262
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 1,
"type": "PrimitiveInt",
"pos": {
"0": 14,
"1": 43
},
"size": [
203.1999969482422,
40.368401303242536
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "value",
"type": "INT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Int"
},
"widgets_values": [10]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -1,48 +0,0 @@
{
"last_node_id": 15,
"last_link_id": 10,
"nodes": [
{
"id": 15,
"type": "DevToolsRemoteWidgetNode",
"pos": [
495,
735
],
"size": [
315,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsRemoteWidgetNode"
},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8008869919566275,
"offset": [
538.9801226576359,
-55.24554581806672
]
}
},
"version": 0.4
}

View File

@@ -1,101 +0,0 @@
{
"last_node_id": 4,
"last_link_id": 2,
"nodes": [
{
"id": 3,
"type": "EmptyLatentImage",
"pos": [
380.51641845703125,
191.39659118652344
],
"size": [
315,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "breadth",
"type": "INT",
"widget": {
"name": "breadth"
},
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 4,
"type": "PrimitiveNode",
"pos": [
73.6164321899414,
197.9966278076172
],
"size": [
210,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"widget": {
"name": "bredth"
},
"links": [
2
]
}
],
"title": "breadth",
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
512,
"fixed"
]
}
],
"links": [
[
2,
4,
0,
3,
0,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"VHS_latentpreview": true,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": false,
"VHS_KeepIntermediate": false
},
"version": 0.4
}

View File

@@ -1,110 +0,0 @@
{
"last_node_id": 25,
"last_link_id": 33,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [160, 240],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": []
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": []
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [33]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["v1-5-pruned-emaonly.safetensors"]
},
{
"id": 19,
"type": "VAEDecode",
"pos": [623.0897216796875, 324.64453125],
"size": [210, 46],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": 33
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
}
],
"links": [[33, 4, 2, 19, 1, "VAE"]],
"floatingLinks": [
{
"id": 6,
"origin_id": 4,
"origin_slot": 2,
"target_id": -1,
"target_slot": -1,
"type": "VAE",
"parentId": 1
}
],
"groups": [],
"config": {},
"extra": {
"reroutes": [
{
"id": 1,
"pos": [545.4541015625, 295.85760498046875],
"linkIds": [],
"floating": {
"slotType": "output"
}
},
{
"id": 2,
"pos": [543.8283081054688, 355.45849609375],
"linkIds": [33]
}
],
"linkExtensions": [
{
"id": 33,
"parentId": 2
}
]
},
"version": 0.4
}

View File

@@ -1,35 +0,0 @@
{
"last_node_id": 11,
"last_link_id": 9,
"nodes": [
{
"id": 11,
"type": "DevToolsNodeWithBooleanInput",
"pos": [
0,
30
],
"size": [
315,
58
],
"flags": {
"collapsed": false
},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithBooleanInput"
},
"widgets_values": [
false
]
}
],
"links": [],
"groups": [],
"config": {},
"version": 0.4
}

View File

@@ -1,32 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "LoadAudio",
"pos": [41.5296516418457, 16.930862426757812],
"size": [315, 82],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "AUDIO",
"type": "AUDIO",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadAudio"
},
"widgets_values": [null, ""]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -1,46 +0,0 @@
{
"last_node_id": 10,
"last_link_id": 10,
"nodes": [
{
"id": 10,
"type": "LoadImage",
"pos": [
50,
50
],
"size": [
315,
314
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"example.png",
"image"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -1,43 +0,0 @@
{
"last_node_id": 10,
"last_link_id": 9,
"nodes": [
{
"id": 10,
"type": "DevToolsNodeWithSeedInput",
"pos": [
20,
50
],
"size": [
315,
82
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithSeedInput"
},
"widgets_values": [
0,
"randomize"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => {

View File

@@ -2,7 +2,7 @@ import {
ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
} from './fixtures/ComfyPage'
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { Palette } from '../src/types/colorPaletteTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes: Record<string, Palette> = {
obsidian: {
@@ -152,10 +152,9 @@ test.describe('Color Palette', () => {
// doesn't update the store immediately.
await comfyPage.setup()
await comfyPage.loadWorkflow('every_node_color')
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark-all-colors.png'
'custom-color-palette-obsidian-dark.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
@@ -233,7 +232,7 @@ test.describe('Node Color Adjustments', () => {
const workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
for (const node of JSON.parse(workflow ?? '{}').nodes) {
for (const node of JSON.parse(workflow).nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Keybindings', () => {
test('Should execute command', async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Copy Paste', () => {
test('Can copy and paste node', async ({ comfyPage }) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1,7 +1,7 @@
import { Locator, expect } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { Keybinding } from '../src/types/keyBindingTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
@@ -66,76 +66,9 @@ test.describe('Missing models warning', () => {
}, comfyPage.url)
})
test('Should display a warning when missing models are found', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
})
test('Should display a warning when missing models are found in node properties', async ({
comfyPage
}) => {
// Load workflow that has a node with models metadata at the node level
await comfyPage.loadWorkflow('missing_models_from_node_properties')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
})
test('Should not display a warning when no missing models are found', async ({
comfyPage
}) => {
const modelFoldersRes = {
status: 200,
body: JSON.stringify([
{
name: 'text_encoders',
folders: ['ComfyUI/models/text_encoders']
}
])
}
comfyPage.page.route(
'**/api/experiment/models',
(route) => route.fulfill(modelFoldersRes),
{ times: 1 }
)
// Reload page to trigger indexing of model folders
await comfyPage.setup()
const clipModelsRes = {
status: 200,
body: JSON.stringify([
{
name: 'fake_model.safetensors',
pathIndex: 0
}
])
}
comfyPage.page.route(
'**/api/experiment/models/text_encoders',
(route) => route.fulfill(clipModelsRes),
{ times: 1 }
)
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).not.toBeVisible()
})
// Flaky test after parallelization
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
test.skip('Should download missing model when clicking download button', async ({
test.skip('Should display a warning when missing models are found', async ({
comfyPage
}) => {
// The fake_model.safetensors is served by
@@ -153,49 +86,6 @@ test.describe('Missing models warning', () => {
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
})
test.describe('Do not show again checkbox', () => {
let checkbox: Locator
let closeButton: Locator
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',
true
)
await comfyPage.loadWorkflow('missing_models')
checkbox = comfyPage.page.getByLabel("Don't show this again")
closeButton = comfyPage.page.getByLabel('Close')
})
test('Should disable warning dialog when checkbox is checked', async ({
comfyPage
}) => {
await checkbox.click()
const changeSettingPromise = comfyPage.page.waitForRequest(
'**/api/settings/Comfy.Workflow.ShowMissingModelsWarning'
)
await closeButton.click()
await changeSettingPromise
const settingValue = await comfyPage.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning'
)
expect(settingValue).toBe(false)
})
test('Should keep warning dialog enabled when checkbox is unchecked', async ({
comfyPage
}) => {
await closeButton.click()
const settingValue = await comfyPage.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning'
)
expect(settingValue).toBe(true)
})
})
})
test.describe('Settings', () => {
@@ -258,7 +148,7 @@ test.describe('Settings', () => {
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('New Blank Workflow')
.getByLabel('Comfy.NewBlankWorkflow')
.getByLabel('Save')
await saveButton.click()
@@ -309,21 +199,3 @@ test.describe('Feedback dialog', () => {
await expect(feedbackHeader).not.toBeVisible()
})
})
test.describe('Error dialog', () => {
test('Should display an error dialog when graph configure fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
const graph = window['graph']
graph.configure = () => {
throw new Error('Error on configure!')
}
})
await comfyPage.loadWorkflow('default')
const errorDialog = comfyPage.page.locator('.error-dialog-content')
await expect(errorDialog).toBeVisible()
})
})

View File

@@ -1,12 +1,12 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('DOM Widget', () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible()
expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
})
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { SettingParams } from '../../src/types/settingTypes'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -135,90 +134,6 @@ test.describe('Topbar commands', () => {
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true)
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
})
test.describe('Passing through attrs to setting components', () => {
const testCases: Array<{
config: Partial<SettingParams>
selector: string
}> = [
{
config: {
type: 'boolean',
defaultValue: true
},
selector: '.p-toggleswitch.p-component'
},
{
config: {
type: 'number',
defaultValue: 10
},
selector: '.p-inputnumber input'
},
{
config: {
type: 'slider',
defaultValue: 10
},
selector: '.p-slider.p-component'
},
{
config: {
type: 'combo',
defaultValue: 'foo',
options: ['foo', 'bar', 'baz']
},
selector: '.p-select.p-component'
},
{
config: {
type: 'text',
defaultValue: 'Hello'
},
selector: '.p-inputtext'
},
{
config: {
type: 'color',
defaultValue: '#000000'
},
selector: '.p-colorpicker-preview'
}
] as const
for (const { config, selector } of testCases) {
test(`${config.type} component should respect disabled attr`, async ({
comfyPage
}) => {
await comfyPage.page.evaluate((config) => {
window['app'].registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
name: 'Test',
// The `disabled` attr is common to all settings components
attrs: { disabled: true },
...config
}
]
})
}, config)
await comfyPage.settingDialog.open()
const component = comfyPage.settingDialog.root
.getByText('TestSetting Test')
.locator(selector)
const isDisabled = await component.evaluate((el) =>
el.tagName === 'INPUT'
? (el as HTMLInputElement).disabled
: el.classList.contains('p-disabled')
)
expect(isDisabled).toBe(true)
})
}
})
})
test.describe('About panel', () => {
@@ -280,63 +195,5 @@ test.describe('Topbar commands', () => {
await comfyPage.confirmDialog.click('confirm')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
})
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['value'] = 'foo'
window['app'].extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
window['value'] = value
})
})
await comfyPage.confirmDialog.click('reject')
expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull()
})
})
test.describe('Selection Toolbox', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('Should allow adding commands to selection toolbox', async ({
comfyPage
}) => {
// Register an extension with a selection toolbox command
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'test.selection.command',
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
window['selectionCommandExecuted'] = true
}
}
],
getSelectionToolboxCommands: () => ['test.selection.command']
})
})
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
// Click the command button in the selection toolbox
const toolboxButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-star)'
)
await toolboxButton.click()
// Verify the command was executed
expect(
await comfyPage.page.evaluate(() => window['selectionCommandExecuted'])
).toBe(true)
})
})
})

View File

@@ -1,97 +0,0 @@
import type { Mouse } from '@playwright/test'
import type { ComfyPage } from './ComfyPage'
import type { Position } from './types'
/**
* Used for drag and drop ops
* @see
* - {@link Mouse.down}
* - {@link Mouse.move}
* - {@link Mouse.up}
*/
export interface DragOptions {
button?: 'left' | 'right' | 'middle'
clickCount?: number
steps?: number
}
/**
* Wraps mouse drag and drop to work with a canvas-based app.
*
* Requires the next frame animated before and after all steps, giving the
* canvas time to render the changes before screenshots are taken.
*/
export class ComfyMouse implements Omit<Mouse, 'move'> {
static defaultSteps = 5
static defaultOptions: DragOptions = { steps: ComfyMouse.defaultSteps }
constructor(readonly comfyPage: ComfyPage) {}
/** The normal Playwright {@link Mouse} property from {@link ComfyPage.page}. */
get mouse() {
return this.comfyPage.page.mouse
}
async nextFrame() {
await this.comfyPage.nextFrame()
}
/** Drags from current location to a new location and hovers there (no pointerup event) */
async drag(to: Position, options = ComfyMouse.defaultOptions) {
const { steps, ...downOptions } = options
await this.mouse.down(downOptions)
await this.nextFrame()
await this.move(to, { steps })
await this.nextFrame()
}
async drop(options = ComfyMouse.defaultOptions) {
await this.mouse.up(options)
await this.nextFrame()
}
async dragAndDrop(
from: Position,
to: Position,
options = ComfyMouse.defaultOptions
) {
const { steps } = options
await this.nextFrame()
await this.move(from, { steps })
await this.drag(to, options)
await this.drop(options)
}
/** @see {@link Mouse.move} */
async move(to: Position, options = ComfyMouse.defaultOptions) {
await this.mouse.move(to.x, to.y, options)
await this.nextFrame()
}
//#region Pass-through
async click(...args: Parameters<Mouse['click']>) {
return await this.mouse.click(...args)
}
async dblclick(...args: Parameters<Mouse['dblclick']>) {
return await this.mouse.dblclick(...args)
}
async down(...args: Parameters<Mouse['down']>) {
return await this.mouse.down(...args)
}
async up(...args: Parameters<Mouse['up']>) {
return await this.mouse.up(...args)
}
async wheel(...args: Parameters<Mouse['wheel']>) {
return await this.mouse.wheel(...args)
}
//#endregion Pass-through
}

View File

@@ -1,17 +1,15 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
import type { NodeId } from '../../src/schemas/comfyWorkflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import type { NodeId } from '../../src/types/comfyWorkflow'
import type { KeyCombo } from '../../src/types/keyBindingTypes'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { SettingDialog } from './components/SettingDialog'
import {
@@ -214,10 +212,6 @@ export class ComfyPage {
`Failed to setup workflows directory: ${await resp.text()}`
)
}
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}
async setupUser(username: string) {
@@ -284,8 +278,8 @@ export class ComfyPage {
await this.page.addStyleTag({
content: `
* {
font-family: 'Roboto Mono', 'Noto Color Emoji';
}`
font-family: 'Roboto Mono', 'Noto Color Emoji';
}`
})
await this.page.waitForFunction(() => document.fonts.ready)
await this.page.waitForFunction(
@@ -463,14 +457,7 @@ export class ComfyPage {
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
options: {
dropPosition?: Position
} = {}
) {
const { dropPosition = { x: 100, y: 100 } } = options
async dragAndDropFile(fileName: string) {
const filePath = this.assetPath(fileName)
// Read the file content
@@ -480,65 +467,39 @@ export class ComfyPage {
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'
}
const fileType = getFileType(fileName)
await this.page.evaluate(
async ({ buffer, fileName, fileType, dropPosition }) => {
async ({ buffer, fileName, fileType }) => {
const file = new File([new Uint8Array(buffer)], fileName, {
type: fileType
})
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
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 = {
const dropEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer,
clientX: dropPosition.x,
clientY: dropPosition.y
}
const dragOverEvent = new DragEvent('dragover', eventOptions)
const dropEvent = new DragEvent('drop', eventOptions)
dataTransfer
})
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)
}
}
document.dispatchEvent(dropEvent)
},
{ buffer: [...new Uint8Array(buffer)], fileName, fileType, dropPosition }
{ buffer: [...new Uint8Array(buffer)], fileName, fileType }
)
await this.nextFrame()
@@ -589,20 +550,11 @@ export class ComfyPage {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
async connectEdge(
options: {
reverse?: boolean
} = {}
) {
const { reverse = false } = options
const start = reverse
? this.clipTextEncodeNode1InputSlot
: this.loadCheckpointNodeClipOutputSlot
const end = reverse
? this.loadCheckpointNodeClipOutputSlot
: this.clipTextEncodeNode1InputSlot
await this.dragAndDrop(start, end)
async connectEdge() {
await this.dragAndDrop(
this.loadCheckpointNodeClipOutputSlot,
this.clipTextEncodeNode1InputSlot
)
}
async adjustWidgetValue() {
@@ -694,18 +646,6 @@ export class ComfyPage {
await this.nextFrame()
}
async selectNodes(nodeTitles: string[]) {
await this.page.keyboard.down('Control')
for (const nodeTitle of nodeTitles) {
const nodes = await this.getNodeRefsByTitle(nodeTitle)
for (const node of nodes) {
await node.click('title')
}
}
await this.page.keyboard.up('Control')
await this.nextFrame()
}
async select2Nodes() {
// Select 2 CLIP nodes.
await this.page.keyboard.down('Control')
@@ -895,24 +835,12 @@ export class ComfyPage {
(
await this.page.evaluate((type) => {
return window['app'].graph.nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
.filter((n) => n.type === type)
.map((n) => n.id)
}, type)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getNodeRefsByTitle(title: string): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate((title) => {
return window['app'].graph.nodes
.filter((n: LGraphNode) => n.title === title)
.map((n: LGraphNode) => n.id)
}, title)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getFirstNodeRef(): Promise<NodeReference | null> {
const id = await this.page.evaluate(() => {
return window['app'].graph.nodes[0]?.id
@@ -956,14 +884,11 @@ export class ComfyPage {
}
}
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
comfyPage: async ({ page, request }, use, testInfo) => {
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ page, request }, use) => {
const comfyPage = new ComfyPage(page, request)
const { parallelIndex } = testInfo
const { parallelIndex } = comfyPageFixture.info()
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
@@ -971,18 +896,15 @@ export const comfyPageFixture = base.extend<{
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
// Hide canvas menu/info/selection toolbox by default.
// Hide canvas menu/info by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true
'Comfy.userId': userId
})
} catch (e) {
console.error(e)
@@ -990,10 +912,6 @@ export const comfyPageFixture = base.extend<{
await comfyPage.setup()
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
use(comfyMouse)
}
})

View File

@@ -3,10 +3,6 @@ import { Page } from '@playwright/test'
export class SettingDialog {
constructor(public readonly page: Page) {}
get root() {
return this.page.locator('div.settings-container')
}
async open() {
const button = this.page.locator('button.comfy-settings-btn:visible')
await button.click()

View File

@@ -95,6 +95,18 @@ export class WorkflowsSidebarTab extends SidebarTab {
return this.page.locator('.workflows-sidebar-tab')
}
get browseGalleryButton() {
return this.root.locator('.browse-templates-button')
}
get newBlankWorkflowButton() {
return this.root.locator('.new-blank-workflow-button')
}
get openWorkflowButton() {
return this.root.locator('.open-workflow-button')
}
async getOpenedWorkflowNames() {
return await this.root
.locator('.comfyui-workflows-open .node-label')

View File

@@ -1,17 +1,10 @@
import type { Page } from '@playwright/test'
import type { NodeId } from '../../../src/schemas/comfyWorkflowSchema'
import type { NodeId } from '../../../src/types/comfyWorkflow'
import { ManageGroupNode } from '../../helpers/manageGroupNode'
import type { ComfyPage } from '../ComfyPage'
import type { Position, Size } from '../types'
export const getMiddlePoint = (pos1: Position, pos2: Position) => {
return {
x: (pos1.x + pos2.x) / 2,
y: (pos1.y + pos2.y) / 2
}
}
export class NodeSlotReference {
constructor(
readonly type: 'input' | 'output',
@@ -69,9 +62,6 @@ export class NodeWidgetReference {
readonly node: NodeReference
) {}
/**
* @returns The position of the widget's center
*/
async getPosition(): Promise<Position> {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
@@ -93,42 +83,8 @@ export class NodeWidgetReference {
y: pos[1]
}
}
async click() {
await this.node.comfyPage.canvas.click({
position: await this.getPosition()
})
}
async dragHorizontal(delta: number) {
const pos = await this.getPosition()
const canvas = this.node.comfyPage.canvas
const canvasPos = (await canvas.boundingBox())!
this.node.comfyPage.dragAndDrop(
{
x: canvasPos.x + pos.x,
y: canvasPos.y + pos.y
},
{
x: canvasPos.x + pos.x + delta,
y: canvasPos.y + pos.y
}
)
}
async getValue() {
return 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.`)
return widget.value
},
[this.node.id, this.index] as const
)
}
}
export class NodeReference {
constructor(
readonly id: NodeId,

View File

@@ -8,7 +8,7 @@ import type {
HistoryTaskItem,
TaskItem,
TaskOutput
} from '../../../src/schemas/apiSchema'
} from '../../../src/types/apiTypes'
import type { ComfyPage } from '../ComfyPage'
/** keyof TaskOutput[string] */

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Graph Canvas Menu', () => {
test.beforeEach(async ({ comfyPage }) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
import type { NodeReference } from './fixtures/utils/litegraphUtils'
test.describe('Group Node', () => {
test.describe('Node library sidebar', () => {
@@ -134,37 +134,6 @@ test.describe('Group Node', () => {
expect(await manage2.getSelectedNodeType()).toBe('g2')
})
test('Preserves hidden input configuration when containing duplicate node types', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('group_node_identical_nodes_hidden_inputs')
await comfyPage.nextFrame()
const groupNodeId = 19
const groupNodeName = 'two_VAE_decode'
const totalInputCount = await comfyPage.page.evaluate((nodeName) => {
const {
extra: { groupNodes }
} = window['app'].graph
const { nodes } = groupNodes[nodeName]
return nodes.reduce((acc: number, node) => {
return acc + node.inputs.length
}, 0)
}, groupNodeName)
const visibleInputCount = await comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id)
return node.inputs.length
}, groupNodeId)
// Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each)
expect(totalInputCount).toBe(4)
// Verify there are 2 visible inputs (2 have been hidden in config)
expect(visibleInputCount).toBe(2)
})
test('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,10 +1,4 @@
import { Locator, Page } from '@playwright/test'
import path from 'path'
import {
TemplateInfo,
WorkflowTemplates
} from '../../src/types/workflowTemplateTypes'
export class ComfyTemplates {
readonly content: Locator
@@ -14,20 +8,6 @@ export class ComfyTemplates {
}
async loadTemplate(id: string) {
await this.content
.getByTestId(`template-workflow-${id}`)
.getByRole('img')
.click()
}
async getAllTemplates(): Promise<TemplateInfo[]> {
const templates: WorkflowTemplates[] = await this.page.evaluate(() =>
window['app'].api.getCoreWorkflowTemplates()
)
return templates.flatMap((t) => t.templates)
}
getTemplatePath(filename: string): string {
return path.join('public', 'templates', filename)
await this.content.getByTestId(`template-workflow-${id}`).click()
}
}

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {
@@ -49,7 +49,7 @@ test.describe('Node Interaction', () => {
})
})
test('@2x Can highlight selected', async ({ comfyPage }) => {
test('Can highlight selected', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
@@ -91,24 +91,21 @@ test.describe('Node Interaction', () => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
})
// Test both directions of edge connection.
;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => {
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge({ reverse })
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
})
test('Can disconnect/connect edge', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge()
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
})
})
test('Can move link', async ({ comfyPage }) => {
// Chromium 2x cannot move link.
// See https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/10876381315/job/30176211513
test.skip('Can move link', async ({ comfyPage }) => {
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace
@@ -121,7 +118,10 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png')
})
// Shift drag copy link regressed. See https://github.com/Comfy-Org/ComfyUI_frontend/issues/2941
// Copy link is not working on CI at all
// Chromium 2x recognize it as dragging canvas.
// Chromium triggers search box after link release. The link is indeed copied.
// See https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/10876381315/job/30176211513
test.skip('Can copy link by shift-drag existing link', async ({
comfyPage
}) => {
@@ -140,14 +140,20 @@ test.describe('Node Interaction', () => {
})
test('Auto snap&highlight when dragging link over node', async ({
comfyPage,
comfyMouse
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true)
await comfyMouse.move(comfyPage.clipTextEncodeNode1InputSlot)
await comfyMouse.drag(comfyPage.clipTextEncodeNode2InputSlot)
await comfyPage.page.mouse.move(
comfyPage.clipTextEncodeNode1InputSlot.x,
comfyPage.clipTextEncodeNode1InputSlot.y
)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(
comfyPage.clipTextEncodeNode2InputSlot.x,
comfyPage.clipTextEncodeNode2InputSlot.y
)
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
})
})
@@ -324,12 +330,16 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
})
test('Can pin/unpin nodes', async ({ comfyPage }) => {
// Somehow this test fails on GitHub Actions. It works locally.
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
comfyPage
}) => {
await comfyPage.select2Nodes()
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png')
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
})
@@ -496,24 +506,24 @@ test.describe('Canvas Interaction', () => {
expect(await getCursorStyle()).toBe('default')
})
test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
test('Can pan when dragging a link', async ({ comfyPage }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
await comfyMouse.move(posSlot1)
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
await comfyPage.page.mouse.down()
const posEmpty = comfyPage.emptySpace
await comfyMouse.drag(posEmpty)
await comfyPage.page.mouse.move(posEmpty.x, posEmpty.y)
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')
await comfyPage.page.keyboard.down('Space')
await comfyMouse.mouse.move(posEmpty.x + 100, posEmpty.y + 100)
await comfyPage.page.mouse.move(posEmpty.x + 100, posEmpty.y + 100)
// Canvas should be panned.
await expect(comfyPage.canvas).toHaveScreenshot(
'panning-when-dragging-link.png'
)
await comfyPage.page.keyboard.up('Space')
await comfyMouse.move(posEmpty)
await comfyPage.page.mouse.move(posEmpty.x, posEmpty.y)
// Should be back to dragging link mode when space is released.
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link2.png')
await comfyMouse.drop()
await comfyPage.page.mouse.up()
})
test('Can pan very far and back', async ({ comfyPage }) => {
@@ -678,7 +688,7 @@ test.describe('Load duplicate workflow', () => {
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
await comfyPage.loadWorkflow('single_ksampler')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Some files were not shown because too many files have changed in this diff Show More