Compare commits
25 Commits
task-runne
...
sidebar-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
867c50f7cb | ||
|
|
89027ea969 | ||
|
|
ce9cfdb975 | ||
|
|
ef191033c8 | ||
|
|
293f4295a8 | ||
|
|
3252d62edf | ||
|
|
1dfcc7a0d4 | ||
|
|
f48594fbd5 | ||
|
|
9263330379 | ||
|
|
214f48a6c4 | ||
|
|
f8ba0ab24f | ||
|
|
b2ef66e058 | ||
|
|
95a4fe7e08 | ||
|
|
95cec85c3f | ||
|
|
bc6630742b | ||
|
|
3b679f1194 | ||
|
|
52933e13f5 | ||
|
|
44f900ef56 | ||
|
|
7a5d39f41f | ||
|
|
1d0ae76f8c | ||
|
|
a8ac7296c2 | ||
|
|
4aa04d1419 | ||
|
|
8160ca0342 | ||
|
|
da936d69b6 | ||
|
|
7eaa54fe3f |
43
.cursorrules
@@ -1,43 +0,0 @@
|
||||
// Vue 3 Composition API .cursorrules
|
||||
|
||||
// Vue 3 Composition API best practices
|
||||
const vue3CompositionApiBestPractices = [
|
||||
"Use setup() function for component logic",
|
||||
"Utilize ref and reactive for reactive state",
|
||||
"Implement computed properties with computed()",
|
||||
"Use watch and watchEffect for side effects",
|
||||
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
||||
"Utilize provide/inject for dependency injection",
|
||||
]
|
||||
|
||||
// Folder structure
|
||||
const folderStructure = `
|
||||
src/
|
||||
components/
|
||||
constants/
|
||||
hooks/
|
||||
views/
|
||||
stores/
|
||||
services/
|
||||
App.vue
|
||||
main.ts
|
||||
`;
|
||||
|
||||
// Tailwind CSS best practices
|
||||
const tailwindCssBestPractices = [
|
||||
"Use Tailwind CSS for styling",
|
||||
"Implement responsive design with Tailwind CSS",
|
||||
]
|
||||
|
||||
// Additional instructions
|
||||
const additionalInstructions = `
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
6. Use Suspense for async components
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
`;
|
||||
2
.github/workflows/format.yaml
vendored
@@ -20,4 +20,4 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Run Prettier check
|
||||
run: npm run format:check
|
||||
run: npx prettier --check './**/*.{js,ts,tsx,vue}'
|
||||
162
.github/workflows/i18n-custom-nodes.yaml
vendored
@@ -1,162 +0,0 @@
|
||||
name: Update Locales for given custom node repository
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
owner:
|
||||
description: 'Owner of the repository to update locales for'
|
||||
required: true
|
||||
type: string
|
||||
repository:
|
||||
description: 'Repository to update locales for'
|
||||
required: true
|
||||
type: string
|
||||
fork_owner:
|
||||
description: 'Owner of the forked repository'
|
||||
required: false
|
||||
type: string
|
||||
default: 'Comfy-Org'
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'comfyanonymous/ComfyUI'
|
||||
path: 'ComfyUI'
|
||||
ref: master
|
||||
- name: Checkout ComfyUI_frontend
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
||||
path: 'ComfyUI_frontend'
|
||||
- name: Checkout ComfyUI_devtools
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
- name: Checkout custom node repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
||||
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install ComfyUI requirements
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
pip install -r requirements.txt
|
||||
pip install wait-for-it
|
||||
shell: bash
|
||||
working-directory: ComfyUI
|
||||
- name: Install custom node requirements
|
||||
run: |
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
shell: bash
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
- name: Build & Install ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
rm -rf ../ComfyUI/web/*
|
||||
mv dist/* ../ComfyUI/web/
|
||||
shell: bash
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
python main.py --cpu --multi-user &
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
shell: bash
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Capture base i18n
|
||||
run: npx tsx scripts/diff-i18n capture
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
run: npm run locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Diff base vs updated i18n
|
||||
run: npx tsx scripts/diff-i18n diff
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update i18n in custom node repository
|
||||
run: |
|
||||
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
|
||||
install -d "$LOCALE_DIR"
|
||||
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
|
||||
- name: Check and create fork of custom node repository
|
||||
run: |
|
||||
# Try to fork the repository
|
||||
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
|
||||
echo "Fork failed - repository might already be forked"
|
||||
# Exit 0 to prevent the workflow from failing
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Enable workflows on the forked repository
|
||||
gh api \
|
||||
--method PUT \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
|
||||
-F can_approve_pull_request_reviews=true \
|
||||
-F default_workflow_permissions="write" \
|
||||
-F enabled=true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
- name: Commit changes
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
# Create and switch to new branch
|
||||
git checkout -b update-locales
|
||||
|
||||
# Stage and commit changes
|
||||
git add -A
|
||||
git commit -m "Update locales"
|
||||
|
||||
- name: Install SSH key For PUSH
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
# PR private key from action server
|
||||
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
|
||||
# github public key to confirm it's github server
|
||||
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
||||
|
||||
- name: Push changes
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
# Force push to create the branch
|
||||
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
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
# Create PR using gh cli
|
||||
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
48
.github/workflows/i18n-node-defs.yaml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Update Node Definitions Locales
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
trigger_type:
|
||||
description: 'Type of trigger (manual or automatic)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'manual'
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n -- scripts/collect-i18n-node-defs.ts
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
run: npm run locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: "Update locales for node definitions"
|
||||
title: "Update locales for node definitions"
|
||||
body: |
|
||||
Automated PR to update locales for node definitions
|
||||
|
||||
This PR was created automatically by the frontend update workflow.
|
||||
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
|
||||
base: main
|
||||
labels: dependencies
|
||||
path: ComfyUI_frontend
|
||||
40
.github/workflows/i18n.yaml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Update Locales
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, master, dev* ]
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
# Don't run on fork PRs
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
run: npm run locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Commit updated locales
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git fetch origin ${{ github.head_ref }}
|
||||
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
working-directory: ComfyUI_frontend
|
||||
20
.github/workflows/release.yaml
vendored
@@ -25,8 +25,6 @@ jobs:
|
||||
id: current_version
|
||||
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
|
||||
- name: Build project
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
@@ -43,21 +41,3 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
make_latest: "true"
|
||||
publish_types:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Release')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm run build:types
|
||||
- name: Publish package
|
||||
run: npm publish --access public
|
||||
working-directory: ./dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
3
.github/workflows/test-ui.yaml
vendored
@@ -19,7 +19,8 @@ jobs:
|
||||
- name: Run Jest tests
|
||||
run: |
|
||||
npm run test:generate
|
||||
npm run test:jest -- --verbose
|
||||
npm run test:generate:examples
|
||||
npm run test:jest:fast -- --verbose
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
playwright-tests-chromium:
|
||||
|
||||
9
.gitignore
vendored
@@ -16,8 +16,6 @@ dist-ssr
|
||||
.vscode/*
|
||||
*.code-workspace
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tailwind.json
|
||||
!.vscode/settings.json.default
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
@@ -41,9 +39,4 @@ browser_tests/*/*-win32.png
|
||||
|
||||
.env
|
||||
|
||||
dist.zip
|
||||
|
||||
/temp/
|
||||
|
||||
# Generated JSON Schemas
|
||||
/schemas/
|
||||
dist.zip
|
||||
17
.i18nrc.cjs
@@ -1,17 +0,0 @@
|
||||
// This file is intentionally kept in CommonJS format (.cjs)
|
||||
// to resolve compatibility issues with dependencies that require CommonJS.
|
||||
// Do not convert this file to ESModule format unless all dependencies support it.
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko', '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.
|
||||
`
|
||||
});
|
||||
15
.prettierrc
@@ -3,16 +3,5 @@
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
|
||||
"options": {
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"printWidth": 80
|
||||
}
|
||||
5
.vscode/settings.json.default
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/tailwind.json"
|
||||
]
|
||||
}
|
||||
55
.vscode/tailwind.json
vendored
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
CODEOWNERS
@@ -1,17 +0,0 @@
|
||||
# Admins
|
||||
* @Comfy-Org/comfy_frontend_devs
|
||||
|
||||
# Maintainers
|
||||
*.md @Comfy-Org/comfy_maintainer
|
||||
/tests-ui/ @Comfy-Org/comfy_maintainer
|
||||
/browser_tests/ @Comfy-Org/comfy_maintainer
|
||||
/.env_example @Comfy-Org/comfy_maintainer
|
||||
|
||||
# Translations (AIGODLIKE team + shinshin86)
|
||||
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
|
||||
# Load 3D extension
|
||||
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
|
||||
|
||||
# Mask Editor extension
|
||||
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs
|
||||
211
README.md
@@ -52,31 +52,17 @@ pause
|
||||
|
||||
### Stable Release
|
||||
|
||||
Stable releases are published bi-weekly in the ComfyUI main repository.
|
||||
Stable releases are published weekly in the ComfyUI main repository, aligned with ComfyUI backend's stable release schedule.
|
||||
|
||||
#### Feature Freeze
|
||||
|
||||
There will be a 2-day feature freeze before each stable release. During this period, no new major features will be merged.
|
||||
|
||||
## Release Summary
|
||||
|
||||
### Major features
|
||||
|
||||
<details id='feature-native-translation'>
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
|
||||
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
|
||||
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
</details>
|
||||
|
||||
<details id='feature-mask-editor'>
|
||||
<summary>v1.4: New mask editor</summary>
|
||||
|
||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details id='feature-integrated-server-terminal'>
|
||||
<details>
|
||||
<summary>v1.3.22: Integrated server terminal</summary>
|
||||
|
||||
Press Ctrl + ` to toggle integrated terminal.
|
||||
@@ -84,7 +70,7 @@ Press Ctrl + ` to toggle integrated terminal.
|
||||
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
|
||||
</details>
|
||||
|
||||
<details id='feature-keybinding-customization'>
|
||||
<details>
|
||||
<summary>v1.3.7: Keybinding customization</summary>
|
||||
|
||||
## Basic UI
|
||||
@@ -101,7 +87,7 @@ https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-node-library-sidebar'>
|
||||
<details>
|
||||
<summary>v1.2.4: Node library sidebar tab</summary>
|
||||
|
||||
#### Drag & Drop
|
||||
@@ -111,13 +97,13 @@ https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
|
||||
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
</details>
|
||||
|
||||
<details id='feature-queue-sidebar'>
|
||||
<details>
|
||||
<summary>v1.2.0: Queue/History sidebar tab</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
|
||||
</details>
|
||||
|
||||
<details id='feature-node-search'>
|
||||
<details>
|
||||
<summary>v1.1.0: Node search box</summary>
|
||||
|
||||
#### Fuzzy search & Node preview
|
||||
@@ -129,26 +115,26 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details id='feature-nested-group'>
|
||||
<details>
|
||||
<summary>v1.3.32: **Litegraph** Nested group</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
|
||||
</details>
|
||||
|
||||
<details id='feature-group-selection'>
|
||||
<details>
|
||||
<summary>v1.3.24: **Litegraph** Group selection</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
|
||||
</details>
|
||||
|
||||
<details id='feature-toggle-link-visibility'>
|
||||
<details>
|
||||
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-auto-widget-conversion'>
|
||||
<details>
|
||||
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
|
||||
|
||||
Dropping a link of correct type on node widget will automatically convert the widget to input.
|
||||
@@ -157,7 +143,7 @@ Dropping a link of correct type on node widget will automatically convert the wi
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-pan-mode'>
|
||||
<details>
|
||||
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
|
||||
|
||||
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
|
||||
@@ -167,42 +153,42 @@ or by holding the space key.
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-shift-drag-link-creation'>
|
||||
<details>
|
||||
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-optional-input-donuts'>
|
||||
<details>
|
||||
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-group-title-edit'>
|
||||
<details>
|
||||
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-group-selection-shortcut'>
|
||||
<details>
|
||||
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-node-title-edit'>
|
||||
<details>
|
||||
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-drag-multi-link'>
|
||||
<details>
|
||||
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
|
||||
@@ -211,7 +197,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-auto-connect-link'>
|
||||
<details>
|
||||
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
|
||||
|
||||
#### Before
|
||||
@@ -221,7 +207,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
|
||||
</details>
|
||||
|
||||
<details id='feature-hide-text-overflow'>
|
||||
<details>
|
||||
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
|
||||
@@ -229,55 +215,6 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
### Developer APIs
|
||||
|
||||
<details>
|
||||
<summary>v1.6.13: prompt/confirm/alert replacements for ComfyUI desktop</summary>
|
||||
|
||||
Several browser-only APIs are not available in ComfyUI desktop's electron environment.
|
||||
|
||||
- `window.prompt`
|
||||
- `window.confirm`
|
||||
- `window.alert`
|
||||
|
||||
Please use the following APIs as replacements.
|
||||
|
||||
```js
|
||||
// window.prompt
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: 'Test Prompt',
|
||||
message: 'Test Prompt Message'
|
||||
})
|
||||
.then((value: string) => {
|
||||
// Do something with the value user entered
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
```js
|
||||
// window.confirm
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
})
|
||||
.then((value: boolean) => {
|
||||
// Do something with the value user entered
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
```js
|
||||
// window.alert
|
||||
window['app'].extensionManager.toast
|
||||
.addAlert("Test Alert")
|
||||
```
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.34: Register about panel badges</summary>
|
||||
|
||||
@@ -298,7 +235,7 @@ app.registerExtension({
|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-bottom-panel-tabs'>
|
||||
<details>
|
||||
<summary>v1.3.22: Register bottom panel tabs</summary>
|
||||
|
||||
```js
|
||||
@@ -321,7 +258,7 @@ app.registerExtension({
|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-settings'>
|
||||
<details>
|
||||
<summary>v1.3.22: New settings API</summary>
|
||||
|
||||
Legacy settings API.
|
||||
@@ -367,7 +304,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-commands-keybindings'>
|
||||
<details>
|
||||
<summary>v1.3.7: Register commands and keybindings</summary>
|
||||
|
||||
Extensions can call the following API to register commands and keybindings. Do
|
||||
@@ -396,7 +333,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-topbar-menu'>
|
||||
<details>
|
||||
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
|
||||
|
||||
Extensions can call the following API to register custom topbar menu items.
|
||||
@@ -425,7 +362,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||

|
||||
</details>
|
||||
|
||||
<details id='extension-api-toast'>
|
||||
<details>
|
||||
<summary>v1.2.27: Extension API to add toast message</summary>i
|
||||
|
||||
Extensions can call the following API to add toast messages.
|
||||
@@ -443,7 +380,7 @@ Documentation of all supported options can be found here: <https://primevue.org/
|
||||

|
||||
</details>
|
||||
|
||||
<details id='extension-api-sidebar-tab'>
|
||||
<details>
|
||||
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
|
||||
|
||||
Extensions now can call the following API to register a sidebar tab.
|
||||
@@ -477,7 +414,6 @@ We will support custom icons later.
|
||||
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
|
||||
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
|
||||
- [zod](https://zod.dev/) for schema validation
|
||||
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
@@ -520,7 +456,8 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
|
||||
- `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:generate` to fetch `tests-ui/data/object_info.json`
|
||||
- `npm run test:jest` to execute all unit tests.
|
||||
- `npm run test:generate:examples` to extract the example workflows
|
||||
- `npm run test` to execute all unit tests.
|
||||
|
||||
### Component Test
|
||||
|
||||
@@ -543,86 +480,14 @@ This repo is using litegraph package hosted on <https://github.com/Comfy-Org/lit
|
||||
|
||||
This will replace the litegraph package in this repo with the local litegraph repo.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
- en (English)
|
||||
- zh (中文)
|
||||
- ru (Русский)
|
||||
- ja (日本語)
|
||||
- ko (한국어)
|
||||
- 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.
|
||||
|
||||
## Publish release to ComfyUI main repo
|
||||
|
||||
Run following command to publish a release to ComfyUI main repo. The script will create a new branch and do a commit to `web/` folder by checkout `dist.zip`
|
||||
from GitHub release.
|
||||
|
||||
- `python scripts/main_repo_release.py <path_to_comfyui_main_repo> <version>`
|
||||
|
||||
@@ -34,8 +34,6 @@ There are two ways to run the tests:
|
||||
```
|
||||
This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
|
||||
|
||||
To run the same test multiple times in Playwright's UI mode, you must launch the main ComfyUI process with the `--multi-user` flag.
|
||||
|
||||

|
||||
|
||||
## Screenshot Expectations
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Response } from '@playwright/test'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
import { comfyPageFixture } from './fixtures/ComfyPage'
|
||||
import { webSocketFixture } from './fixtures/ws.ts'
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [20, 50],
|
||||
"size": [400, 200],
|
||||
"flags": { "collapsed": true },
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null,
|
||||
"localized_name": "clip"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null,
|
||||
"localized_name": "CONDITIONING"
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["Should not be displayed."]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 828 B |
|
Before Width: | Height: | Size: 488 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 152 B After Width: | Height: | Size: 195 KiB |
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "DevToolsSimpleSlider",
|
||||
"pos": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"label": "FLOAT"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsSimpleSlider"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.5
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
{
|
||||
"last_node_id": 16,
|
||||
"last_link_id": 18,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [620, 260],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "Reroute",
|
||||
"pos": [510, 280],
|
||||
"size": [75, 26],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [18],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [160, 240],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [13],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[13, 4, 2, 13, 0, "*"],
|
||||
[18, 13, 0, 12, 1, "VAE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Browser tab title', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './fixtures/ComfyPage'
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Palette } from '../src/types/colorPaletteTypes'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
const customColorPalettes: Record<string, Palette> = {
|
||||
const customColorPalettes = {
|
||||
obsidian: {
|
||||
version: 102,
|
||||
id: 'obsidian',
|
||||
@@ -129,61 +127,23 @@ const customColorPalettes: Record<string, Palette> = {
|
||||
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
|
||||
}
|
||||
}
|
||||
},
|
||||
// A custom light theme with fg color red
|
||||
light_red: {
|
||||
id: 'light_red',
|
||||
name: 'Light Red',
|
||||
light_theme: true,
|
||||
colors: {
|
||||
node_slot: {},
|
||||
litegraph_base: {},
|
||||
comfy_base: {
|
||||
'fg-color': '#ff0000'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Color Palette', () => {
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
|
||||
// doesn't update the store immediately.
|
||||
await comfyPage.setup()
|
||||
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-light-red.png'
|
||||
)
|
||||
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
|
||||
})
|
||||
|
||||
test('Can add custom color palette', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate((p) => {
|
||||
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
|
||||
}, customColorPalettes.obsidian_dark)
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
)
|
||||
// Legacy `custom_` prefix is still supported
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Copy Paste', () => {
|
||||
@@ -11,14 +10,6 @@ test.describe('Copy Paste', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
|
||||
})
|
||||
|
||||
test('Can copy and paste node with link', async ({ comfyPage }) => {
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.page.keyboard.press('Control+Shift+V')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png')
|
||||
})
|
||||
|
||||
test('Can copy and paste text', async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
@@ -1,6 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { Keybinding } from '../src/types/keyBindingTypes'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Load workflow warning', () => {
|
||||
@@ -44,18 +42,6 @@ test.describe('Execution error', () => {
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(executionError).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can display Issue Report form', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.getByLabel('Help Fix This').click()
|
||||
const issueReportForm = comfyPage.page.getByText(
|
||||
'Submit Error Report (Optional)'
|
||||
)
|
||||
await expect(issueReportForm).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Missing models warning', () => {
|
||||
@@ -91,12 +77,8 @@ test.describe('Missing models warning', () => {
|
||||
test.describe('Settings', () => {
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsContent = comfyPage.page.locator('.settings-content')
|
||||
await expect(settingsContent).toBeVisible()
|
||||
const isUsableHeight = await settingsContent.evaluate(
|
||||
(el) => el.clientHeight > 30
|
||||
)
|
||||
expect(isUsableHeight).toBeTruthy()
|
||||
const searchBox = comfyPage.page.locator('.settings-content')
|
||||
await expect(searchBox).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can open settings with hotkey', async ({ comfyPage }) => {
|
||||
@@ -112,90 +94,8 @@ test.describe('Settings', () => {
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const maxSpeed = 2.5
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
await test.step('Setting should persist', async () => {
|
||||
test.step('Setting should persist', async () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
test('Should persist keybinding setting', async ({ comfyPage }) => {
|
||||
// Open the settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container')
|
||||
|
||||
// Open the keybinding tab
|
||||
await comfyPage.page.getByLabel('Keybinding').click()
|
||||
await comfyPage.page.waitForSelector(
|
||||
'[placeholder="Search Keybindings..."]'
|
||||
)
|
||||
|
||||
// Focus the 'New Blank Workflow' row
|
||||
const newBlankWorkflowRow = comfyPage.page.locator('tr', {
|
||||
has: comfyPage.page.getByRole('cell', { name: 'New Blank Workflow' })
|
||||
})
|
||||
await newBlankWorkflowRow.click()
|
||||
|
||||
// Click edit button
|
||||
const editKeybindingButton = newBlankWorkflowRow.locator('.pi-pencil')
|
||||
await editKeybindingButton.click()
|
||||
|
||||
// Set new keybinding
|
||||
const input = comfyPage.page.getByPlaceholder('Press keys for new binding')
|
||||
await input.press('Alt+n')
|
||||
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
'**/api/settings/Comfy.Keybinding.NewBindings'
|
||||
)
|
||||
|
||||
// Save keybinding
|
||||
const saveButton = comfyPage.page
|
||||
.getByLabel('Comfy.NewBlankWorkflow')
|
||||
.getByLabel('Save')
|
||||
await saveButton.click()
|
||||
|
||||
const request = await requestPromise
|
||||
const expectedSetting: Keybinding = {
|
||||
commandId: 'Comfy.NewBlankWorkflow',
|
||||
combo: {
|
||||
key: 'n',
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Feedback dialog', () => {
|
||||
test('Should open from topmenu help command', async ({ comfyPage }) => {
|
||||
// Open feedback dialog from top menu
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
// Verify feedback dialog content is visible
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
await expect(feedbackHeader).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should close when close button clicked', async ({ comfyPage }) => {
|
||||
// Open feedback dialog
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
|
||||
// Close feedback dialog
|
||||
await comfyPage.page
|
||||
.getByLabel('', { exact: true })
|
||||
.getByLabel('Close')
|
||||
.click()
|
||||
await feedbackHeader.waitFor({ state: 'hidden' })
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(feedbackHeader).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
125
browser_tests/documentationSidebar.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
const nodeDef = {
|
||||
title: 'TestNodeAdvancedDoc'
|
||||
}
|
||||
|
||||
test.describe('Documentation Sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
const currentThemeId = await comfyPage.menu.getThemeId()
|
||||
if (currentThemeId !== 'dark') {
|
||||
await comfyPage.menu.toggleTheme()
|
||||
}
|
||||
})
|
||||
|
||||
test('Sidebar registered', async ({ comfyPage }) => {
|
||||
await expect(
|
||||
comfyPage.page.locator('.documentation-tab-button')
|
||||
).toBeVisible()
|
||||
})
|
||||
test('Parses help for basic node', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
//Check that each independently parsed element exists
|
||||
await expect(docPane).toContainText('Load Checkpoint')
|
||||
await expect(docPane).toContainText('Loads a diffusion model')
|
||||
await expect(docPane).toContainText('The name of the checkpoint')
|
||||
await expect(docPane).toContainText('The VAE model used')
|
||||
})
|
||||
test('Responds to hovering over node', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.page.mouse.move(321, 593)
|
||||
const tooltipTimeout = 500
|
||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.sidebar-content-container>div>div:nth-child(4)')
|
||||
).toBeFocused()
|
||||
})
|
||||
test('Updates when a new node is selected', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.page.mouse.click(557, 440)
|
||||
await expect(docPane).not.toContainText('Load Checkpoint')
|
||||
await expect(docPane).toContainText('CLIP Text Encode (Prompt)')
|
||||
await expect(docPane).toContainText('The text to be encoded')
|
||||
await expect(docPane).toContainText(
|
||||
'A conditioning containing the embedded text'
|
||||
)
|
||||
})
|
||||
test('Responds to a change in theme', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.menu.toggleTheme()
|
||||
await expect(docPane).toHaveScreenshot(
|
||||
'documentation-sidebar-light-theme.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
test.describe('Advanced Description tests', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
//register test node and add to graph
|
||||
await comfyPage.page.evaluate(async (node) => {
|
||||
const app = window['app']
|
||||
await app.registerNodeDef(node.name, node)
|
||||
app.addNodeOnGraph(node)
|
||||
}, advDocNode)
|
||||
})
|
||||
test('Description displays as raw html', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container>div')
|
||||
await expect(docPane).toHaveJSProperty(
|
||||
'innerHTML',
|
||||
advDocNode.description[1]
|
||||
)
|
||||
})
|
||||
test('selected function', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const app = window['app']
|
||||
const desc =
|
||||
LiteGraph.registered_node_types['Test_AdvancedDescription'].nodeData
|
||||
.description
|
||||
desc[2].select = (element, name, value) => {
|
||||
element.children[0].innerText = name + ' ' + value
|
||||
}
|
||||
})
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.page.mouse.move(307, 80)
|
||||
const tooltipTimeout = 500
|
||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
|
||||
await expect(docPane).toContainText('int_input 0')
|
||||
})
|
||||
})
|
||||
|
||||
const advDocNode = {
|
||||
display_name: 'Node With Advanced Description',
|
||||
name: 'Test_AdvancedDescription',
|
||||
input: {
|
||||
required: {
|
||||
int_input: [
|
||||
'INT',
|
||||
{ tooltip: "an input tooltip that won't be displayed in sidebar" }
|
||||
]
|
||||
}
|
||||
},
|
||||
output: ['INT'],
|
||||
output_name: ['int_output'],
|
||||
output_tooltips: ["An output tooltip that won't be displayed in the sidebar"],
|
||||
output_is_list: false,
|
||||
description: [
|
||||
'A node with description in the advanced format',
|
||||
`
|
||||
A long form description that will be displayed in the sidebar.
|
||||
<div doc_title="INT">Can include arbitrary html</div>
|
||||
<div doc_title="int_input">or out of order widgets</div>
|
||||
`,
|
||||
{}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -1,27 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
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')
|
||||
|
||||
expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
|
||||
const multilineTextAreas = comfyPage.page.locator('.comfy-multiline-input')
|
||||
const firstMultiline = multilineTextAreas.first()
|
||||
const lastMultiline = multilineTextAreas.last()
|
||||
|
||||
await expect(firstMultiline).toBeVisible()
|
||||
await expect(lastMultiline).toBeVisible()
|
||||
|
||||
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of nodes) {
|
||||
await node.click('collapse')
|
||||
}
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { expect, Locator } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
@@ -158,42 +157,4 @@ test.describe('Topbar commands', () => {
|
||||
expect(await badge.textContent()).toContain('Test Badge')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Dialog', () => {
|
||||
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: 'Test Prompt',
|
||||
message: 'Test Prompt Message'
|
||||
})
|
||||
.then((value: string) => {
|
||||
window['value'] = value
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.fillPromptDialog('Hello, world!')
|
||||
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(
|
||||
'Hello, world!'
|
||||
)
|
||||
})
|
||||
|
||||
test('Should allow showing a confirmation dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
})
|
||||
.then((value: boolean) => {
|
||||
window['value'] = value
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.confirmDialog.click('confirm')
|
||||
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
||||
import type { Page, Locator, APIRequestContext } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
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 { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
QueueSidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
import type { Position, Size } from './types'
|
||||
import { NodeReference } from './utils/litegraphUtils'
|
||||
import TaskHistory from './utils/taskHistory'
|
||||
|
||||
dotenv.config()
|
||||
import type { Position, Size } from './types'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
class ComfyMenu {
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||
private _queueTab: QueueSidebarTab | null = null
|
||||
private _topbar: Topbar | null = null
|
||||
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly themeToggleButton: Locator
|
||||
public readonly saveButton: Locator
|
||||
@@ -45,23 +36,15 @@ class ComfyMenu {
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
|
||||
return this._nodeLibraryTab
|
||||
return new NodeLibrarySidebarTab(this.page)
|
||||
}
|
||||
|
||||
get workflowsTab() {
|
||||
this._workflowsTab ??= new WorkflowsSidebarTab(this.page)
|
||||
return this._workflowsTab
|
||||
}
|
||||
|
||||
get queueTab() {
|
||||
this._queueTab ??= new QueueSidebarTab(this.page)
|
||||
return this._queueTab
|
||||
return new WorkflowsSidebarTab(this.page)
|
||||
}
|
||||
|
||||
get topbar() {
|
||||
this._topbar ??= new Topbar(this.page)
|
||||
return this._topbar
|
||||
return new Topbar(this.page)
|
||||
}
|
||||
|
||||
async toggleTheme() {
|
||||
@@ -92,33 +75,7 @@ type FolderStructure = {
|
||||
[key: string]: FolderStructure | string
|
||||
}
|
||||
|
||||
type KeysOfType<T, Match> = {
|
||||
[K in keyof T]: T[K] extends Match ? K : never
|
||||
}[keyof T]
|
||||
|
||||
class ConfirmDialog {
|
||||
public readonly delete: Locator
|
||||
public readonly overwrite: Locator
|
||||
public readonly reject: Locator
|
||||
public readonly confirm: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.delete = page.locator('button.p-button[aria-label="Delete"]')
|
||||
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
|
||||
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
|
||||
this.confirm = page.locator('button.p-button[aria-label="Confirm"]')
|
||||
}
|
||||
|
||||
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
|
||||
const loc = this[locator]
|
||||
await expect(loc).toBeVisible()
|
||||
await loc.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyPage {
|
||||
private _history: TaskHistory | null = null
|
||||
|
||||
public readonly url: string
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
public readonly canvas: Locator
|
||||
@@ -137,7 +94,6 @@ export class ComfyPage {
|
||||
public readonly actionbar: ComfyActionbar
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -162,7 +118,6 @@ export class ComfyPage {
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -252,11 +207,6 @@ export class ComfyPage {
|
||||
}
|
||||
}
|
||||
|
||||
setupHistory(): TaskHistory {
|
||||
this._history ??= new TaskHistory(this)
|
||||
return this._history
|
||||
}
|
||||
|
||||
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
await this.goto()
|
||||
if (clearStorage) {
|
||||
@@ -369,6 +319,11 @@ export class ComfyPage {
|
||||
}, settingId)
|
||||
}
|
||||
|
||||
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
await this.page.reload({ timeout: 15000 })
|
||||
await this.setup({ clearStorage })
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto(this.url)
|
||||
}
|
||||
@@ -535,17 +490,6 @@ export class ComfyPage {
|
||||
return { x: 427, y: 98 }
|
||||
}
|
||||
|
||||
get promptDialogInput() {
|
||||
return this.page.locator('.p-dialog-content input[type="text"]')
|
||||
}
|
||||
|
||||
async fillPromptDialog(value: string) {
|
||||
await this.promptDialogInput.fill(value)
|
||||
await this.page.keyboard.press('Enter')
|
||||
await this.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async disconnectEdge() {
|
||||
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
|
||||
}
|
||||
@@ -796,27 +740,28 @@ export class ComfyPage {
|
||||
)
|
||||
}
|
||||
|
||||
async clickDialogButton(prompt: string, buttonText: string = 'Yes') {
|
||||
async confirmDialog(prompt: string, text: string = 'Yes') {
|
||||
const modal = this.page.locator(
|
||||
`.comfy-modal-content:has-text("${prompt}")`
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
await modal
|
||||
.locator('.comfyui-button', {
|
||||
hasText: buttonText
|
||||
hasText: text
|
||||
})
|
||||
.click()
|
||||
await expect(modal).toBeHidden()
|
||||
}
|
||||
|
||||
async convertAllNodesToGroupNode(groupNodeName: string) {
|
||||
this.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
})
|
||||
await this.canvas.press('Control+a')
|
||||
const node = await this.getFirstNodeRef()
|
||||
await node!.clickContextMenuOption('Convert to Group Node')
|
||||
await this.fillPromptDialog(groupNodeName)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async convertOffsetToCanvas(pos: [number, number]) {
|
||||
return this.page.evaluate((pos) => {
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
|
||||
@@ -825,11 +770,6 @@ export class ComfyPage {
|
||||
async getNodeRefById(id: NodeId) {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodes() {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
})
|
||||
}
|
||||
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
|
||||
return Promise.all(
|
||||
(
|
||||
@@ -871,17 +811,6 @@ export class ComfyPage {
|
||||
.activeWorkflow?.isModified
|
||||
})
|
||||
}
|
||||
async getExportedWorkflow({ api = false }: { api?: boolean } = {}) {
|
||||
return this.page.evaluate(async (api) => {
|
||||
return (await window['app'].graphToPrompt())[api ? 'output' : 'workflow']
|
||||
}, api)
|
||||
}
|
||||
async setFocusMode(focusMode: boolean) {
|
||||
await this.page.evaluate((focusMode) => {
|
||||
window['app'].extensionManager.focusMode = focusMode
|
||||
}, focusMode)
|
||||
await this.nextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
@@ -893,23 +822,18 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
const userId = await comfyPage.setupUser(username)
|
||||
comfyPage.userIds[parallelIndex] = userId
|
||||
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Disabled',
|
||||
// Hide canvas menu/info by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': 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
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Disabled',
|
||||
// Hide canvas menu/info by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': 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
|
||||
})
|
||||
await comfyPage.setup()
|
||||
await use(comfyPage)
|
||||
}
|
||||
@@ -944,19 +868,5 @@ const makeMatcher = function <T>(
|
||||
export const comfyExpect = expect.extend({
|
||||
toBePinned: makeMatcher((n) => n.isPinned(), 'pinned'),
|
||||
toBeBypassed: makeMatcher((n) => n.isBypassed(), 'bypassed'),
|
||||
toBeCollapsed: makeMatcher((n) => n.isCollapsed(), 'collapsed'),
|
||||
async toHaveFocus(locator: Locator, options = { timeout: 256 }) {
|
||||
const isFocused = await locator.evaluate(
|
||||
(el) => el === document.activeElement
|
||||
)
|
||||
|
||||
await expect(async () => {
|
||||
expect(isFocused).toBe(!this.isNot)
|
||||
}).toPass(options)
|
||||
|
||||
return {
|
||||
pass: isFocused,
|
||||
message: () => `Expected element to ${isFocused ? 'not ' : ''}be focused.`
|
||||
}
|
||||
}
|
||||
toBeCollapsed: makeMatcher((n) => n.isCollapsed(), 'collapsed')
|
||||
})
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
public readonly url: string,
|
||||
public readonly page: Page
|
||||
) {}
|
||||
|
||||
get selectionUrl() {
|
||||
return this.url + '/user-select'
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this.page.locator('#comfy-user-selection')
|
||||
}
|
||||
|
||||
get newUserInput() {
|
||||
return this.container.locator('#new-user-input')
|
||||
}
|
||||
|
||||
get existingUserSelect() {
|
||||
return this.container.locator('#existing-user-select')
|
||||
}
|
||||
|
||||
get nextButton() {
|
||||
return this.container.getByText('Next')
|
||||
}
|
||||
}
|
||||
|
||||
export const userSelectPageFixture = base.extend<{
|
||||
userSelectPage: UserSelectPage
|
||||
}>({
|
||||
userSelectPage: async ({ page }, use) => {
|
||||
const userSelectPage = new UserSelectPage(
|
||||
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
|
||||
page
|
||||
)
|
||||
await use(userSelectPage)
|
||||
}
|
||||
})
|
||||
@@ -3,13 +3,6 @@ import { Locator, Page } from '@playwright/test'
|
||||
export class ComfyNodeSearchFilterSelectionPanel {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
get header() {
|
||||
return this.page
|
||||
.getByRole('dialog')
|
||||
.locator('div')
|
||||
.filter({ hasText: 'Add node filter condition' })
|
||||
}
|
||||
|
||||
async selectFilterType(filterType: string) {
|
||||
await this.page
|
||||
.locator(
|
||||
|
||||
@@ -22,12 +22,6 @@ class SidebarTab {
|
||||
}
|
||||
await this.tabButton.click()
|
||||
}
|
||||
async close() {
|
||||
if (!this.tabButton.isVisible()) {
|
||||
return
|
||||
}
|
||||
await this.tabButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeLibrarySidebarTab extends SidebarTab {
|
||||
@@ -91,36 +85,32 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
super(page, 'workflows')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.workflows-sidebar-tab')
|
||||
}
|
||||
|
||||
get browseGalleryButton() {
|
||||
return this.root.locator('.browse-templates-button')
|
||||
return this.page.locator('.browse-templates-button')
|
||||
}
|
||||
|
||||
get newBlankWorkflowButton() {
|
||||
return this.root.locator('.new-blank-workflow-button')
|
||||
return this.page.locator('.new-blank-workflow-button')
|
||||
}
|
||||
|
||||
get openWorkflowButton() {
|
||||
return this.root.locator('.open-workflow-button')
|
||||
return this.page.locator('.open-workflow-button')
|
||||
}
|
||||
|
||||
async getOpenedWorkflowNames() {
|
||||
return await this.root
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-open .node-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveWorkflowName() {
|
||||
return await this.root
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async getTopLevelSavedWorkflowNames() {
|
||||
return await this.root
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-browse .node-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
@@ -132,13 +122,13 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
}
|
||||
|
||||
getOpenedItem(name: string) {
|
||||
return this.root.locator('.comfyui-workflows-open .node-label', {
|
||||
return this.page.locator('.comfyui-workflows-open .node-label', {
|
||||
hasText: name
|
||||
})
|
||||
}
|
||||
|
||||
getPersistedItem(name: string) {
|
||||
return this.root.locator('.comfyui-workflows-browse .node-label', {
|
||||
return this.page.locator('.comfyui-workflows-browse .node-label', {
|
||||
hasText: name
|
||||
})
|
||||
}
|
||||
@@ -152,132 +142,4 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
await this.page.keyboard.press('Enter')
|
||||
await this.page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
async insertWorkflow(locator: Locator) {
|
||||
await locator.click({ button: 'right' })
|
||||
await this.page
|
||||
.locator('.p-contextmenu-item-content', { hasText: 'Insert' })
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class QueueSidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'queue')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.sidebar-content-container', { hasText: 'Queue' })
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
return this.root.locator('[data-virtual-grid-item]')
|
||||
}
|
||||
|
||||
get visibleTasks() {
|
||||
return this.tasks.locator('visible=true')
|
||||
}
|
||||
|
||||
get clearButton() {
|
||||
return this.root.locator('.clear-all-button')
|
||||
}
|
||||
|
||||
get collapseTasksButton() {
|
||||
return this.getToggleExpandButton(false)
|
||||
}
|
||||
|
||||
get expandTasksButton() {
|
||||
return this.getToggleExpandButton(true)
|
||||
}
|
||||
|
||||
get noResultsPlaceholder() {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open()
|
||||
return this.root.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async close() {
|
||||
await super.close()
|
||||
await this.root.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
async expandTasks() {
|
||||
await this.expandTasksButton.click()
|
||||
await this.collapseTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async collapseTasks() {
|
||||
await this.collapseTasksButton.click()
|
||||
await this.expandTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async waitForTasks() {
|
||||
return Promise.all([
|
||||
this.tasks.first().waitFor({ state: 'visible' }),
|
||||
this.tasks.last().waitFor({ state: 'visible' })
|
||||
])
|
||||
}
|
||||
|
||||
async scrollTasks(direction: 'up' | 'down') {
|
||||
const scrollToEl =
|
||||
direction === 'up' ? this.tasks.last() : this.tasks.first()
|
||||
await scrollToEl.scrollIntoViewIfNeeded()
|
||||
await this.waitForTasks()
|
||||
}
|
||||
|
||||
async clearTasks() {
|
||||
await this.clearButton.click()
|
||||
const confirmButton = this.page.getByLabel('Delete')
|
||||
await confirmButton.click()
|
||||
await this.noResultsPlaceholder.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/** Set the width of the tab (out of 100). Must call before opening the tab */
|
||||
async setTabWidth(width: number) {
|
||||
if (width < 0 || width > 100) {
|
||||
throw new Error('Width must be between 0 and 100')
|
||||
}
|
||||
return this.page.evaluate((width) => {
|
||||
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
|
||||
}, width)
|
||||
}
|
||||
|
||||
getTaskPreviewButton(taskIndex: number) {
|
||||
return this.tasks.nth(taskIndex).getByRole('button')
|
||||
}
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@ export class Topbar {
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveTabName(): Promise<string> {
|
||||
return this.page
|
||||
.locator('.workflow-tabs .p-togglebutton-checked')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async openSubmenuMobile() {
|
||||
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
||||
}
|
||||
@@ -46,14 +40,7 @@ export class Topbar {
|
||||
return this._saveWorkflow(workflowName, 'Save As')
|
||||
}
|
||||
|
||||
exportWorkflow(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Export')
|
||||
}
|
||||
|
||||
async _saveWorkflow(
|
||||
workflowName: string,
|
||||
command: 'Save' | 'Save As' | 'Export'
|
||||
) {
|
||||
async _saveWorkflow(workflowName: string, command: 'Save' | 'Save As') {
|
||||
await this.triggerTopbarCommand(['Workflow', command])
|
||||
await this.getSaveDialog().fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../src/types/comfyWorkflow'
|
||||
import { ManageGroupNode } from '../../helpers/manageGroupNode'
|
||||
import type { NodeId } from '../../../src/types/comfyWorkflow'
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
import type { Position, Size } from '../types'
|
||||
|
||||
@@ -234,8 +233,10 @@ export class NodeReference {
|
||||
await ctx.getByText(optionText).click()
|
||||
}
|
||||
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
|
||||
this.comfyPage.page.once('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
})
|
||||
await this.clickContextMenuOption('Convert to Group Node')
|
||||
await this.comfyPage.fillPromptDialog(groupNodeName)
|
||||
await this.comfyPage.nextFrame()
|
||||
const nodes = await this.comfyPage.getNodeRefsByType(
|
||||
`workflow>${groupNodeName}`
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import type { Request, Route } from 'playwright'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type {
|
||||
HistoryTaskItem,
|
||||
TaskItem,
|
||||
TaskOutput
|
||||
} from '../../../src/types/apiTypes'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
|
||||
/** keyof TaskOutput[string] */
|
||||
type OutputFileType = 'images' | 'audio' | 'animated'
|
||||
|
||||
const DEFAULT_IMAGE = 'example.webp'
|
||||
|
||||
const getFilenameParam = (request: Request) => {
|
||||
const url = new URL(request.url())
|
||||
return url.searchParams.get('filename') || DEFAULT_IMAGE
|
||||
}
|
||||
|
||||
const getContentType = (filename: string, fileType: OutputFileType) => {
|
||||
const subtype = path.extname(filename).slice(1)
|
||||
switch (fileType) {
|
||||
case 'images':
|
||||
return `image/${subtype}`
|
||||
case 'audio':
|
||||
return `audio/${subtype}`
|
||||
case 'animated':
|
||||
return `video/${subtype}`
|
||||
}
|
||||
}
|
||||
|
||||
const setQueueIndex = (task: TaskItem) => {
|
||||
task.prompt[0] = TaskHistory.queueIndex++
|
||||
}
|
||||
|
||||
const setPromptId = (task: TaskItem) => {
|
||||
task.prompt[1] = uuidv4()
|
||||
}
|
||||
|
||||
export default class TaskHistory {
|
||||
static queueIndex = 0
|
||||
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
||||
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
},
|
||||
taskType: 'History'
|
||||
}
|
||||
private tasks: HistoryTaskItem[] = []
|
||||
private outputContentTypes: Map<string, string> = new Map()
|
||||
|
||||
constructor(readonly comfyPage: ComfyPage) {}
|
||||
|
||||
private loadAsset: (filename: string) => Buffer = _.memoize(
|
||||
(filename: string) => {
|
||||
const filePath = this.comfyPage.assetPath(filename)
|
||||
return fs.readFileSync(filePath)
|
||||
}
|
||||
)
|
||||
|
||||
private async handleGetHistory(route: Route) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(this.tasks)
|
||||
})
|
||||
}
|
||||
|
||||
private async handleGetView(route: Route) {
|
||||
const fileName = getFilenameParam(route.request())
|
||||
if (!this.outputContentTypes.has(fileName)) route.continue()
|
||||
|
||||
const asset = this.loadAsset(fileName)
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: this.outputContentTypes.get(fileName),
|
||||
body: asset,
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Content-Length': asset.byteLength.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async setupRoutes() {
|
||||
return this.comfyPage.page.route(
|
||||
/.*\/api\/(view|history)(\?.*)?$/,
|
||||
async (route) => {
|
||||
const request = route.request()
|
||||
const method = request.method()
|
||||
|
||||
const isViewReq = request.url().includes('view') && method === 'GET'
|
||||
if (isViewReq) return this.handleGetView(route)
|
||||
|
||||
const isHistoryPath = request.url().includes('history')
|
||||
const isGetHistoryReq = isHistoryPath && method === 'GET'
|
||||
if (isGetHistoryReq) return this.handleGetHistory(route)
|
||||
|
||||
const isClearReq =
|
||||
method === 'POST' &&
|
||||
isHistoryPath &&
|
||||
request.postDataJSON()?.clear === true
|
||||
if (isClearReq) return this.clearTasks()
|
||||
|
||||
return route.continue()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private createOutputs(
|
||||
filenames: string[],
|
||||
filetype: OutputFileType
|
||||
): TaskOutput {
|
||||
return filenames.reduce((outputs, filename, i) => {
|
||||
const nodeId = `${i + 1}`
|
||||
outputs[nodeId] = {
|
||||
[filetype]: [{ filename, subfolder: '', type: 'output' }]
|
||||
}
|
||||
const contentType = getContentType(filename, filetype)
|
||||
this.outputContentTypes.set(filename, contentType)
|
||||
return outputs
|
||||
}, {})
|
||||
}
|
||||
|
||||
private addTask(task: HistoryTaskItem) {
|
||||
setPromptId(task)
|
||||
setQueueIndex(task)
|
||||
this.tasks.unshift(task) // Tasks are added to the front of the queue
|
||||
}
|
||||
|
||||
clearTasks(): this {
|
||||
this.tasks = []
|
||||
return this
|
||||
}
|
||||
|
||||
withTask(
|
||||
outputFilenames: string[],
|
||||
outputFiletype: OutputFileType = 'images',
|
||||
overrides: Partial<HistoryTaskItem> = {}
|
||||
): this {
|
||||
this.addTask({
|
||||
...TaskHistory.defaultTask,
|
||||
outputs: this.createOutputs(outputFilenames, outputFiletype),
|
||||
...overrides
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/** Repeats the last task in the task history a specified number of times. */
|
||||
repeat(n: number): this {
|
||||
for (let i = 0; i < n; i++)
|
||||
this.addTask(structuredClone(this.tasks.at(0)) as HistoryTaskItem)
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Graph Canvas Menu', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
import type { NodeReference } from './fixtures/utils/litegraphUtils'
|
||||
|
||||
@@ -104,36 +103,6 @@ test.describe('Group Node', () => {
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Manage group opens with the correct group selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const makeGroup = async (name, type1, type2) => {
|
||||
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
|
||||
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
|
||||
await node1.click('title')
|
||||
await node2.click('title', {
|
||||
modifiers: ['Shift']
|
||||
})
|
||||
return await node2.convertToGroupNode(name)
|
||||
}
|
||||
|
||||
const group1 = await makeGroup(
|
||||
'g1',
|
||||
'CLIPTextEncode',
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
|
||||
|
||||
const manage1 = await group1.manageGroupNode()
|
||||
await comfyPage.nextFrame()
|
||||
expect(await manage1.getSelectedNodeType()).toBe('g1')
|
||||
await manage1.close()
|
||||
await expect(manage1.root).not.toBeVisible()
|
||||
|
||||
const manage2 = await group2.manageGroupNode()
|
||||
expect(await manage2.getSelectedNodeType()).toBe('g2')
|
||||
})
|
||||
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import type { AutoQueueMode } from '../../src/stores/queueStore'
|
||||
|
||||
export class ComfyActionbar {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
header: Locator
|
||||
|
||||
constructor(
|
||||
readonly page: Page,
|
||||
readonly root: Locator
|
||||
) {
|
||||
this.footer = root.locator('footer')
|
||||
this.header = root.locator('header')
|
||||
}
|
||||
|
||||
async setLabel(name: string, label: string) {
|
||||
@@ -26,11 +23,6 @@ export class ManageGroupNode {
|
||||
await this.footer.getByText('Close').click()
|
||||
}
|
||||
|
||||
async getSelectedNodeType() {
|
||||
const select = this.header.locator('select').first()
|
||||
return await select.inputValue()
|
||||
}
|
||||
|
||||
async selectNode(name: string) {
|
||||
const list = this.root.locator('.comfy-group-manage-list-items')
|
||||
const item = list.getByText(name)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ComfyTemplates {
|
||||
readonly content: Locator
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
@@ -453,9 +452,6 @@ test.describe('Canvas Interaction', () => {
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
await comfyPage.page.mouse.down()
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
// Move mouse should not alter cursor style.
|
||||
await comfyPage.page.mouse.move(10, 20)
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
await comfyPage.page.mouse.up()
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
|
||||
@@ -469,43 +465,6 @@ test.describe('Canvas Interaction', () => {
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
})
|
||||
|
||||
// https://github.com/Comfy-Org/litegraph.js/pull/424
|
||||
test('Properly resets dragging state after pan mode sequence', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const getCursorStyle = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return (
|
||||
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Initial state check
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
|
||||
// Click and hold
|
||||
await comfyPage.page.mouse.down()
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
|
||||
// Press space while holding click
|
||||
await comfyPage.page.keyboard.down('Space')
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
|
||||
// Release click while space is still down
|
||||
await comfyPage.page.mouse.up()
|
||||
expect(await getCursorStyle()).toBe('grab')
|
||||
|
||||
// Release space
|
||||
await comfyPage.page.keyboard.up('Space')
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
|
||||
// Move mouse - cursor should remain default
|
||||
await comfyPage.page.mouse.move(20, 20)
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
})
|
||||
|
||||
test('Can pan when dragging a link', async ({ comfyPage }) => {
|
||||
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
|
||||
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
|
||||
@@ -593,7 +552,7 @@ test.describe('Load workflow', () => {
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
await comfyPage.reload({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
})
|
||||
|
||||
@@ -610,72 +569,11 @@ test.describe('Load workflow', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
await comfyPage.reload({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Restore all open workflows on reload', () => {
|
||||
let workflowA: string
|
||||
let workflowB: string
|
||||
|
||||
const generateUniqueFilename = (extension = '') =>
|
||||
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
workflowA = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
||||
workflowB = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
||||
|
||||
// Wait for localStorage to persist the workflow paths before reloading
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
|
||||
)
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
})
|
||||
|
||||
test('Restores topbar workflow tabs after reload', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Topbar'
|
||||
)
|
||||
const tabs = await comfyPage.menu.topbar.getTabNames()
|
||||
const activeWorkflowName = await comfyPage.menu.topbar.getActiveTabName()
|
||||
|
||||
expect(tabs).toEqual(expect.arrayContaining([workflowA, workflowB]))
|
||||
expect(tabs.indexOf(workflowA)).toBeLessThan(tabs.indexOf(workflowB))
|
||||
expect(activeWorkflowName).toEqual(workflowB)
|
||||
})
|
||||
|
||||
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
const openWorkflows =
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
const activeWorkflowName =
|
||||
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
|
||||
const workflowPathA = `${workflowA}.json`
|
||||
const workflowPathB = `${workflowB}.json`
|
||||
|
||||
expect(openWorkflows).toEqual(
|
||||
expect.arrayContaining([workflowPathA, workflowPathB])
|
||||
)
|
||||
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
|
||||
openWorkflows.indexOf(workflowPathB)
|
||||
)
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
function listenForEvent(): Promise<Event> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Load Workflow in Media', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Menu', () => {
|
||||
@@ -18,7 +17,7 @@ test.describe('Menu', () => {
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('light')
|
||||
|
||||
// Theme id should persist after reload.
|
||||
await comfyPage.setup()
|
||||
await comfyPage.reload()
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('light')
|
||||
|
||||
await comfyPage.menu.toggleTheme()
|
||||
@@ -289,47 +288,6 @@ test.describe('Menu', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Can customize bookmark color after interacting with color options', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open customization dialog
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.getFolder('foo').click({ button: 'right' })
|
||||
await comfyPage.page.getByLabel('Customize').click()
|
||||
|
||||
// Click a color option multiple times
|
||||
const customColorOption = comfyPage.page.locator(
|
||||
'.p-togglebutton-content > .pi-palette'
|
||||
)
|
||||
await customColorOption.click()
|
||||
await customColorOption.click()
|
||||
|
||||
// Use the color picker
|
||||
await comfyPage.page
|
||||
.getByLabel('Customize Folder')
|
||||
.getByRole('textbox')
|
||||
.click()
|
||||
await comfyPage.page.locator('.p-colorpicker-color-background').click()
|
||||
|
||||
// Finalize the customization
|
||||
await comfyPage.page
|
||||
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
|
||||
.click()
|
||||
await comfyPage.page.getByLabel('Confirm').click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the color selection is saved
|
||||
const setting = await comfyPage.getSetting(
|
||||
'Comfy.NodeLibrary.BookmarksCustomization'
|
||||
)
|
||||
await expect(setting).toHaveProperty(['foo/', 'color'])
|
||||
await expect(setting['foo/'].color).not.toBeNull()
|
||||
await expect(setting['foo/'].color).not.toBeUndefined()
|
||||
await expect(setting['foo/'].color).not.toBe('')
|
||||
})
|
||||
|
||||
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
|
||||
@@ -392,6 +350,23 @@ test.describe('Menu', () => {
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
})
|
||||
|
||||
test('Can migrate legacy bookmarks', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [
|
||||
'foo/',
|
||||
'foo/KSampler (Advanced)',
|
||||
'UNKNOWN',
|
||||
'KSampler'
|
||||
])
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
|
||||
await comfyPage.reload()
|
||||
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
|
||||
[]
|
||||
)
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
|
||||
).toEqual(['foo/', 'foo/KSamplerAdvanced', 'KSampler'])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows sidebar', () => {
|
||||
@@ -437,56 +412,6 @@ test.describe('Menu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('Can duplicate workflow', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1.json'])
|
||||
)
|
||||
|
||||
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
|
||||
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json'
|
||||
])
|
||||
|
||||
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json',
|
||||
'*workflow1 (Copy) (3).json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can open workflow after insert', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'single_ksampler.json'
|
||||
})
|
||||
await comfyPage.setup()
|
||||
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow')
|
||||
const originalNodeCount = (await comfyPage.getNodes()).length
|
||||
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
|
||||
await comfyPage.nextFrame()
|
||||
expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1)
|
||||
|
||||
await tab.getPersistedItem('workflow1.json').click()
|
||||
await comfyPage.nextFrame()
|
||||
expect((await comfyPage.getNodes()).length).toEqual(1)
|
||||
})
|
||||
|
||||
test('Can rename nested workflow from opened workflow item', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -527,56 +452,6 @@ test.describe('Menu', () => {
|
||||
).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Exported workflow does not contain localized slot names', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
const exportedWorkflow = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
})
|
||||
expect(exportedWorkflow).toBeDefined()
|
||||
for (const node of exportedWorkflow.nodes) {
|
||||
for (const slot of node.inputs) {
|
||||
expect(slot.localized_name).toBeUndefined()
|
||||
expect(slot.label).toBeUndefined()
|
||||
}
|
||||
for (const slot of node.outputs) {
|
||||
expect(slot.localized_name).toBeUndefined()
|
||||
expect(slot.label).toBeUndefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Can export same workflow with different locales', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
|
||||
// Setup download listener before triggering the export
|
||||
const downloadPromise = comfyPage.page.waitForEvent('download')
|
||||
await comfyPage.menu.topbar.exportWorkflow('exported_default.json')
|
||||
|
||||
// Wait for the download and get the file content
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toBe('exported_default.json')
|
||||
|
||||
// Get the exported workflow content
|
||||
const downloadedContent = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
})
|
||||
|
||||
await comfyPage.setSetting('Comfy.Locale', 'zh')
|
||||
await comfyPage.setup()
|
||||
|
||||
const downloadedContentZh = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
})
|
||||
|
||||
// Compare the exported workflow with the original
|
||||
expect(downloadedContent).toBeDefined()
|
||||
expect(downloadedContent).toEqual(downloadedContentZh)
|
||||
})
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
expect(
|
||||
@@ -584,7 +459,7 @@ test.describe('Menu', () => {
|
||||
).toEqual(['workflow5.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
@@ -619,7 +494,7 @@ test.describe('Menu', () => {
|
||||
)
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
@@ -661,52 +536,6 @@ test.describe('Menu', () => {
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
|
||||
test('Can close saved workflow with command', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
await comfyPage.executeCommand('Workspace.CloseWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
|
||||
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.clickContextMenuItem('Delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows', async ({ comfyPage }) => {
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.clickContextMenuItem('Delete')
|
||||
|
||||
await comfyPage.confirmDialog.click('delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows topbar tabs', () => {
|
||||
@@ -804,204 +633,3 @@ test.describe('Menu', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe.skip('Queue sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('can display tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test('can display tasks after closing then opening', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test.describe('Virtual scroll', () => {
|
||||
const layouts = [
|
||||
{ description: 'Five columns layout', width: 95, rows: 3, cols: 5 },
|
||||
{ description: 'Three columns layout', width: 55, rows: 3, cols: 3 },
|
||||
{ description: 'Two columns layout', width: 40, rows: 3, cols: 2 }
|
||||
]
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(50)
|
||||
.setupRoutes()
|
||||
})
|
||||
|
||||
layouts.forEach(({ description, width, rows, cols }) => {
|
||||
const preRenderedRows = 1
|
||||
const preRenderedTasks = preRenderedRows * cols * 2
|
||||
const visibleTasks = rows * cols
|
||||
const expectRenderLimit = visibleTasks + preRenderedTasks
|
||||
|
||||
test.describe(description, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.setTabWidth(width)
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('should not render items outside of view', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should teardown items after scrolling away', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should re-render items after scrolling away then back', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
await comfyPage.menu.queueTab.scrollTasks('up')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Expand tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// 2-item batch and 3-item batch -> 3 additional items when expanded
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp', 'example.webp', 'example.webp'])
|
||||
.withTask(['example.webp', 'example.webp'])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('can expand tasks with multiple outputs', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount + 3
|
||||
)
|
||||
})
|
||||
|
||||
test('can collapse flat tasks', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
await comfyPage.menu.queueTab.collapseTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Clear tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(6)
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
})
|
||||
|
||||
test('can clear all tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0)
|
||||
expect(
|
||||
await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible()
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('can load new tasks after clearing all', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Gallery', () => {
|
||||
const firstImage = 'example.webp'
|
||||
const secondImage = 'image32x32.webp'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask([secondImage])
|
||||
.withTask([firstImage])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
})
|
||||
|
||||
test('displays gallery image after opening task preview', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Add a new task while the gallery is still open
|
||||
const newImage = 'image64x64.webp'
|
||||
comfyPage.setupHistory().withTask([newImage])
|
||||
await comfyPage.menu.queueTab.triggerTasksUpdate()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
const paths: {
|
||||
description: string
|
||||
path: ('Right' | 'Left')[]
|
||||
end: string
|
||||
}[] = [
|
||||
{ description: 'Right', path: ['Right'], end: secondImage },
|
||||
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
|
||||
{ description: 'Left wrap', path: ['Left'], end: secondImage },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
|
||||
]
|
||||
|
||||
paths.forEach(({ description, path, end }) => {
|
||||
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
|
||||
for (const direction of path)
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
import type { ComfyApp } from '../src/scripts/app'
|
||||
import { NodeBadgeMode } from '../src/types/nodeSource'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Node Badge', () => {
|
||||
test('Can add badge', async ({ comfyPage }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 102 KiB |