Compare commits

..

1 Commits

Author SHA1 Message Date
filtered
c684732199 Allow fast widget conversion from any node 2024-10-31 03:32:26 +11:00
501 changed files with 8470 additions and 68522 deletions

View File

@@ -6,11 +6,6 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
# Note: localhost:8188 does not work.
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# Allow dev server access from remote IP addresses.
# If true, the vite dev server will listen on all addresses, including LAN
# and public addresses.
VITE_REMOTE_DEV=false
# The target ComfyUI checkout directory to deploy the frontend code to.
# The dist directory will be copied to {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev
# Add `--front-end-root {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev`

View File

@@ -1,127 +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: |
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
echo "Fork failed - repository might already be forked"
# Exit 0 to prevent the workflow from failing
exit 0
}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Commit and push changes
run: |
git checkout -b update-locales
git add -A
git commit -m "Update locales"
git remote add org_fork git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git
git push org_fork update-locales
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}

View File

@@ -1,39 +0,0 @@
name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]
jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
# Pipe output so that it doesn't error out on stdout.clearLine
run: npm run locale 2>&1 | cat
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git push origin HEAD:${{ github.head_ref }}
working-directory: ComfyUI_frontend

View File

@@ -41,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 }}

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -15,18 +15,18 @@ jobs:
jest-tests:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
- name: Run Jest tests
run: |
npm run test:generate
npm run test:generate:examples
npm run test:jest:fast -- --verbose
npm test -- --verbose
working-directory: ComfyUI_frontend
playwright-tests-chromium:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
@@ -43,7 +43,7 @@ jobs:
playwright-tests-chromium-2x:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
@@ -60,7 +60,7 @@ jobs:
playwright-tests-mobile-chrome:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

4
.gitignore vendored
View File

@@ -39,6 +39,4 @@ browser_tests/*/*-win32.png
.env
dist.zip
/temp/
dist.zip

View File

@@ -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.
`
});

View File

@@ -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

168
README.md
View File

@@ -52,38 +52,16 @@ 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>
<summary>v1.5: Native translation (i18n)</summary>
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
</details>
<details>
<summary>v1.4: New mask editor</summary>
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
![image](https://github.com/user-attachments/assets/f0ea6ee5-00ee-4e5d-a09c-6938e86a1f17)
</details>
<details>
<summary>v1.3.22: Integrated server terminal</summary>
Press Ctrl + ` to toggle integrated terminal.
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details>
<summary>v1.3.7: Keybinding customization</summary>
@@ -129,18 +107,6 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
### QoL changes
<details>
<summary>v1.3.32: **Litegraph** Nested group</summary>
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
</details>
<details>
<summary>v1.3.24: **Litegraph** Group selection</summary>
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
</details>
<details>
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
@@ -229,26 +195,6 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
### Developer APIs
<details>
<summary>v1.3.34: Register about panel badges</summary>
```js
app.registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
label: 'Test Badge',
url: 'https://example.com',
icon: 'pi pi-box'
}
]
})
```
![image](https://github.com/user-attachments/assets/099e77ee-16ad-4141-b2fc-5e9d5075188b)
</details>
<details>
<summary>v1.3.22: Register bottom panel tabs</summary>
@@ -428,7 +374,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
@@ -440,31 +385,9 @@ hook is used to auto-format code on commit.
Note: The dev server will NOT load any extension from the ComfyUI server. Only
core extensions will be loaded.
- Run `npm install` to install the necessary packages
- Start local ComfyUI backend at `localhost:8188`
- Run `npm run dev` to start the dev server
- Run `npm run dev:electron` to start the dev server with electron API mocked
#### Access dev server on touch devices
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.
After you start the dev server, you should see following logs:
```
> comfyui-frontend@1.3.42 dev
> vite
VITE v5.4.6 ready in 488 ms
➜ Local: http://localhost:5173/
➜ Network: http://172.21.80.1:5173/
➜ Network: http://192.168.2.20:5173/
➜ press h + enter to show help
```
Make sure your desktop machine and touch device are on the same network. On your touch device,
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
### Unit Test
@@ -495,85 +418,6 @@ 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`.

View File

@@ -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.
![Playwright UI Mode](https://github.com/user-attachments/assets/6a1ebef0-90eb-4157-8694-f5ee94d03755)
## Screenshot Expectations

View File

@@ -52,9 +52,7 @@ test.describe('Actionbar', () => {
(n) => n.type === 'EmptyLatentImage'
)
node.widgets[0].value = value
window[
'app'
].extensionManager.workflow.activeWorkflow.changeTracker.checkState()
window['app'].workflowManager.activeWorkflow.changeTracker.checkState()
}, value)
}

View File

@@ -1,227 +0,0 @@
{
"last_node_id": 3,
"last_link_id": 0,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 420,
"1": 130
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": {
"0": 820,
"1": 130
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 1,
"type": "KSampler",
"pos": {
"0": 30,
"1": 130
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
],
"links": [],
"groups": [
{
"id": 0,
"title": "Group",
"bounding": [
406.9701232910156,
59.079444885253906,
335,
345.6000061035156
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "Group Parent",
"bounding": [
796.9703979492188,
14.796443939208984,
355,
399.20001220703125
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 2,
"title": "Group Child",
"bounding": [
806.9703979492188,
58.39643096923828,
335,
345.6000061035156
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -1,90 +0,0 @@
{
"last_node_id": 9,
"last_link_id": 9,
"nodes": [
{
"id": 3,
"type": "KSampler",
"pos": [
37,
98
],
"size": [
315,
262
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
],
"links": [],
"groups": [
{
"id": 1,
"title": "Group",
"bounding": [
23,
23,
900,
825
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,42 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "DevToolsNodeWithStringInput",
"pos": [
15,
48
],
"size": [
315,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithStringInput"
},
"widgets_values": [
""
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -9,22 +9,24 @@ test.describe('Browser tab title', () => {
test('Can display workflow name', async ({ comfyPage }) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
return window['app'].workflowManager.activeWorkflow.name
})
// Note: unsaved workflow name is always prepended with "*".
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
})
// Failing on CI
// Cannot reproduce locally
// Broken by https://github.com/Comfy-Org/ComfyUI_frontend/pull/893
// Release blocker for v1.3.0
test.skip('Can display workflow name with unsaved changes', async ({
comfyPage
}) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
return window['app'].workflowManager.activeWorkflow.name
})
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
// Note: unsaved workflow name is always prepended with "*".
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
await comfyPage.menu.topbar.saveWorkflow('test')
await comfyPage.menu.saveWorkflow('test')
expect(await comfyPage.page.title()).toBe('test - ComfyUI')
const textBox = comfyPage.widgetTextBox
@@ -34,7 +36,7 @@ test.describe('Browser tab title', () => {
// Delete the saved workflow for cleanup.
await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.delete()
window['app'].workflowManager.activeWorkflow.delete()
})
})
})

View File

@@ -16,51 +16,6 @@ async function afterChange(comfyPage: ComfyPage) {
}
test.describe('Change Tracker', () => {
test.describe('Undo/Redo', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setupWorkflowsDirectory({})
})
test('Can undo multiple operations', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
// Save, confirm no errors & workflow modified flag removed
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
expect(await comfyPage.getToastErrorCount()).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())!
await node.click('title')
await node.click('collapse')
await expect(node).toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
await comfyPage.ctrlB()
await expect(node).toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(2)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(1)
await comfyPage.ctrlZ()
await expect(node).not.toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(2)
})
})
test('Can group multiple change actions into a single transaction', async ({
comfyPage
}) => {
@@ -71,7 +26,6 @@ test.describe('Change Tracker', () => {
// Make changes outside set
// Bypass + collapse node
await node.click('title')
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
@@ -85,12 +39,8 @@ test.describe('Change Tracker', () => {
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
// Prevent clicks registering a double-click
await comfyPage.clickEmptySpace()
await node.click('title')
// Run again, but within a change transaction
await beforeChange(comfyPage)
beforeChange(comfyPage)
await node.click('collapse')
await comfyPage.ctrlB()
@@ -98,7 +48,7 @@ test.describe('Change Tracker', () => {
await expect(node).toBeBypassed()
// End transaction
await afterChange(comfyPage)
afterChange(comfyPage)
// Ensure undo reverts both changes
await comfyPage.ctrlZ()
@@ -106,7 +56,7 @@ test.describe('Change Tracker', () => {
await expect(node).not.toBeCollapsed()
})
test('Can nest multiple change transactions without adding undo steps', async ({
test('Can group multiple transaction calls into a single one', async ({
comfyPage
}) => {
const node = (await comfyPage.getFirstNodeRef())!
@@ -130,7 +80,6 @@ test.describe('Change Tracker', () => {
const multipleChanges = async () => {
await beforeChange(comfyPage)
// Call other actions that uses begin/endChange
await node.click('title')
await collapse()
await bypassAndPin()
await afterChange(comfyPage)
@@ -148,20 +97,4 @@ test.describe('Change Tracker', () => {
await expect(node).toBePinned()
await expect(node).toBeCollapsed()
})
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
})
// Click empty space to trigger a change detection.
await comfyPage.clickEmptySpace()
expect(await comfyPage.getUndoQueueSize()).toBe(1)
})
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.pan({ x: 10, y: 10 })
expect(await comfyPage.getUndoQueueSize()).toBe(0)
})
})

View File

@@ -131,8 +131,11 @@ const customColorPalettes = {
}
test.describe('Color Palette', () => {
test('Can show custom color palette', async ({ comfyPage }) => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
})
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -142,19 +145,6 @@ test.describe('Color Palette', () => {
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', 'custom_obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
})
})
test.describe('Node Color Adjustments', () => {
@@ -221,7 +211,7 @@ test.describe('Node Color Adjustments', () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
const node = await comfyPage.getFirstNodeRef()
await node?.clickContextMenuOption('Colors')
await node.clickContextMenuOption('Colors')
})
test('should persist color adjustments when changing custom node colors', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -15,23 +15,15 @@ test.describe('Copy Paste', () => {
await textBox.click()
const originalString = await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await comfyPage.ctrlV()
const resultString = await textBox.inputValue()
expect(resultString).toBe(originalString + originalString)
})
test('Can copy and paste widget value', async ({ comfyPage }) => {
// Copy width value (512) from empty latent node to KSampler's seed.
// KSampler's seed
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await comfyPage.ctrlC(null)
// Empty latent node's width
await comfyPage.canvas.click({
position: {
@@ -39,7 +31,15 @@ test.describe('Copy Paste', () => {
y: 643
}
})
await comfyPage.ctrlV(null)
await comfyPage.ctrlC()
// KSampler's seed
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await comfyPage.ctrlV()
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
})
@@ -51,14 +51,14 @@ test.describe('Copy Paste', () => {
comfyPage
}) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC(null)
await comfyPage.ctrlC()
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await comfyPage.ctrlV()
await expect(comfyPage.canvas).toHaveScreenshot(
'paste-in-text-area-with-node-previously-copied.png'
)
@@ -69,10 +69,10 @@ test.describe('Copy Paste', () => {
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlC()
// Unfocus textbox.
await comfyPage.page.mouse.click(10, 10)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV()
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -36,7 +36,6 @@ test.describe('Execution error', () => {
}) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
// Wait for the element with the .comfy-execution-error selector to be visible
const executionError = comfyPage.page.locator('.comfy-error-report')
@@ -52,9 +51,7 @@ test.describe('Missing models warning', () => {
}, comfyPage.url)
})
// Flaky test after parallelization
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
test.skip('Should display a warning when missing models are found', async ({
test('Should display a warning when missing models are found', async ({
comfyPage
}) => {
// The fake_model.safetensors is served by
@@ -77,12 +74,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 }) => {
@@ -98,7 +91,7 @@ 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)
})
})

View File

@@ -48,7 +48,7 @@ test.describe('Topbar commands', () => {
})
})
const menuItem = comfyPage.menu.topbar.getMenuItem('ext')
const menuItem: Locator = await comfyPage.menu.topbar.getMenuItem('ext')
expect(await menuItem.count()).toBe(0)
})
@@ -80,81 +80,22 @@ test.describe('Topbar commands', () => {
)
})
test.describe('Settings', () => {
test('Should allow adding settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!',
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
}
}
]
})
test('Should allow adding settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!'
}
]
})
// onChange is called when the setting is first added
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!')
await comfyPage.setSetting('TestSetting', 'Hello, universe!')
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!')
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
})
test('Should allow setting boolean settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
name: 'Test Setting',
type: 'boolean',
defaultValue: false,
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
}
}
]
})
})
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false)
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
await comfyPage.settingDialog.open()
await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting')
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true)
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
})
})
test.describe('About panel', () => {
test('Should allow adding badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
label: 'Test Badge',
url: 'https://example.com',
icon: 'pi pi-box'
}
]
})
})
await comfyPage.settingDialog.open()
await comfyPage.settingDialog.goToAboutPanel()
const badge = comfyPage.page.locator('.about-badge').last()
expect(badge).toBeDefined()
expect(await badge.textContent()).toContain('Test Badge')
})
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!')
await comfyPage.setSetting('TestSetting', 'Hello, universe!')
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!')
})
})

View File

@@ -17,10 +17,6 @@ import {
import { Topbar } from './components/Topbar'
import { NodeReference } from './utils/litegraphUtils'
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 {
public readonly sideToolbar: Locator
@@ -75,28 +71,6 @@ 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
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"]')
}
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
const loc = this[locator]
await expect(loc).toBeVisible()
await loc.click()
}
}
export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
@@ -115,16 +89,6 @@ export class ComfyPage {
public readonly menu: ComfyMenu
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[] = []
/** Test user ID for the current context */
get id() {
return this.userIds[comfyPageFixture.info().parallelIndex]
}
constructor(
public readonly page: Page,
@@ -140,8 +104,6 @@ export class ComfyPage {
this.menu = new ComfyMenu(page)
this.actionbar = new ComfyActionbar(page)
this.templates = new ComfyTemplates(page)
this.settingDialog = new SettingDialog(page)
this.confirmDialog = new ConfirmDialog(page)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {
@@ -181,7 +143,7 @@ export class ComfyPage {
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.id}/workflows`
base_path: 'user/default/workflows'
}
}
)
@@ -193,31 +155,6 @@ export class ComfyPage {
}
}
async setupUser(username: string) {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)
const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]
return id ? id : await this.createUser(username)
}
async createUser(username: string) {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})
if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)
return await resp.json()
}
async setupSettings(settings: Record<string, any>) {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
@@ -231,17 +168,12 @@ export class ComfyPage {
}
}
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
async setup() {
await this.goto()
if (clearStorage) {
await this.page.evaluate((id) => {
localStorage.clear()
sessionStorage.clear()
localStorage.setItem('Comfy.userId', id)
}, this.id)
}
await this.goto()
await this.page.evaluate(() => {
localStorage.clear()
sessionStorage.clear()
})
// Unify font for consistent screenshots.
await this.page.addStyleTag({
url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
@@ -343,9 +275,9 @@ export class ComfyPage {
}, settingId)
}
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
async reload() {
await this.page.reload({ timeout: 15000 })
await this.setup({ clearStorage })
await this.setup()
}
async goto() {
@@ -378,12 +310,6 @@ export class ComfyPage {
await this.nextFrame()
}
async getToastErrorCount() {
return await this.page
.locator('.p-toast-message.p-toast-message-error')
.count()
}
async getVisibleToastCount() {
return await this.page.locator('.p-toast:visible').count()
}
@@ -514,10 +440,6 @@ export class ComfyPage {
return { x: 427, y: 98 }
}
get promptDialogInput() {
return this.page.locator('.p-dialog-content input[type="text"]')
}
async disconnectEdge() {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
@@ -557,6 +479,9 @@ export class ComfyPage {
safeSpot = safeSpot || { x: 10, y: 10 }
await this.page.mouse.move(safeSpot.x, safeSpot.y)
await this.page.mouse.down()
// TEMPORARY HACK: Multiple pans open the search menu, so cheat and keep it closed.
// TODO: Fix that (double-click at not-the-same-coordinations should not open the menu)
await this.page.keyboard.press('Escape')
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
await this.page.mouse.up()
await this.nextFrame()
@@ -585,13 +510,8 @@ export class ComfyPage {
await this.nextFrame()
}
async clickContextMenuItem(name: string): Promise<void> {
await this.page.getByRole('menuitem', { name }).click()
await this.nextFrame()
}
async doubleClickCanvas() {
await this.page.mouse.dblclick(10, 10, { delay: 5 })
await this.page.mouse.dblclick(10, 10)
await this.nextFrame()
}
@@ -627,42 +547,43 @@ export class ComfyPage {
await this.nextFrame()
}
async ctrlSend(keyToPress: string, locator: Locator | null = this.canvas) {
const target = locator ?? this.page.keyboard
await target.press(`Control+${keyToPress}`)
async ctrlSend(keyToPress: string) {
await this.page.keyboard.down('Control')
await this.page.keyboard.press(keyToPress)
await this.page.keyboard.up('Control')
await this.nextFrame()
}
async ctrlA(locator?: Locator | null) {
await this.ctrlSend('KeyA', locator)
async ctrlA() {
await this.ctrlSend('KeyA')
}
async ctrlB(locator?: Locator | null) {
await this.ctrlSend('KeyB', locator)
async ctrlB() {
await this.ctrlSend('KeyB')
}
async ctrlC(locator?: Locator | null) {
await this.ctrlSend('KeyC', locator)
async ctrlC() {
await this.ctrlSend('KeyC')
}
async ctrlV(locator?: Locator | null) {
await this.ctrlSend('KeyV', locator)
async ctrlV() {
await this.ctrlSend('KeyV')
}
async ctrlZ(locator?: Locator | null) {
await this.ctrlSend('KeyZ', locator)
async ctrlZ() {
await this.ctrlSend('KeyZ')
}
async ctrlY(locator?: Locator | null) {
await this.ctrlSend('KeyY', locator)
async ctrlY() {
await this.ctrlSend('KeyY')
}
async ctrlArrowUp(locator?: Locator | null) {
await this.ctrlSend('ArrowUp', locator)
async ctrlArrowUp() {
await this.ctrlSend('ArrowUp')
}
async ctrlArrowDown(locator?: Locator | null) {
await this.ctrlSend('ArrowDown', locator)
async ctrlArrowDown() {
await this.ctrlSend('ArrowDown')
}
async closeMenu() {
@@ -690,14 +611,10 @@ export class ComfyPage {
x: nodePos.x + nodeSize.width * ratioX,
y: nodePos.y + nodeSize.height * ratioY
}
// -1 to be inside the node. -2 because nodes currently get an arbitrary +1 to width.
await this.dragAndDrop(
{ x: bottomRight.x - 2, y: bottomRight.y - 1 },
target
)
await this.dragAndDrop(bottomRight, target)
await this.nextFrame()
if (revertAfter) {
await this.dragAndDrop({ x: target.x - 2, y: target.y - 1 }, bottomRight)
await this.dragAndDrop(target, bottomRight)
await this.nextFrame()
}
}
@@ -708,20 +625,14 @@ export class ComfyPage {
revertAfter: boolean = false
) {
const ksamplerPos = {
x: 863,
y: 156
x: 864,
y: 157
}
const ksamplerSize = {
width: 315,
height: 292
}
return this.resizeNode(
ksamplerPos,
ksamplerSize,
percentX,
percentY,
revertAfter
)
this.resizeNode(ksamplerPos, ksamplerSize, percentX, percentY, revertAfter)
}
async resizeLoadCheckpointNode(
@@ -730,14 +641,14 @@ export class ComfyPage {
revertAfter: boolean = false
) {
const loadCheckpointPos = {
x: 26,
y: 444
x: 25,
y: 440
}
const loadCheckpointSize = {
width: 315,
height: 127
width: 320,
height: 120
}
return this.resizeNode(
this.resizeNode(
loadCheckpointPos,
loadCheckpointSize,
percentX,
@@ -752,14 +663,14 @@ export class ComfyPage {
revertAfter: boolean = false
) {
const emptyLatentPos = {
x: 473,
y: 579
x: 475,
y: 580
}
const emptyLatentSize = {
width: 315,
height: 136
width: 303,
height: 132
}
return this.resizeNode(
this.resizeNode(
emptyLatentPos,
emptyLatentSize,
percentX,
@@ -768,29 +679,15 @@ export class ComfyPage {
)
}
async clickDialogButton(prompt: string, buttonText: string = 'Yes') {
const modal = this.page.locator(
`.comfy-modal-content:has-text("${prompt}")`
)
await expect(modal).toBeVisible()
await modal
.locator('.comfyui-button', {
hasText: buttonText
})
.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.promptDialogInput.fill(groupNodeName)
await this.page.keyboard.press('Enter')
await this.promptDialogInput.waitFor({ state: 'hidden' })
await this.nextFrame()
}
async convertOffsetToCanvas(pos: [number, number]) {
return this.page.evaluate((pos) => {
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
@@ -820,65 +717,21 @@ export class ComfyPage {
async moveMouseToEmptyArea() {
await this.page.mouse.move(10, 10)
}
async getUndoQueueSize() {
return this.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.undoQueue.length
})
}
async getRedoQueueSize() {
return this.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.redoQueue.length
})
}
async isCurrentWorkflowModified() {
return this.page.evaluate(() => {
return (window['app'].extensionManager as WorkspaceStore).workflow
.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 }>({
comfyPage: async ({ page, request }, use) => {
const comfyPage = new ComfyPage(page, request)
const { parallelIndex } = comfyPageFixture.info()
const username = `playwright-test-${parallelIndex}`
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({
// 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
})
await comfyPage.setup()
await use(comfyPage)
}
@@ -913,19 +766,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')
})

View File

@@ -1,41 +0,0 @@
import { Page } from 'playwright'
import { test as base } from '@playwright/test'
export class UserSelectPage {
constructor(
public readonly url: string,
public readonly page: Page
) {}
get selectionUrl() {
return this.url + '/user-select'
}
get container() {
return this.page.locator('#comfy-user-selection')
}
get newUserInput() {
return this.container.locator('#new-user-input')
}
get existingUserSelect() {
return this.container.locator('#existing-user-select')
}
get nextButton() {
return this.container.getByText('Next')
}
}
export const userSelectPageFixture = base.extend<{
userSelectPage: UserSelectPage
}>({
userSelectPage: async ({ page }, use) => {
const userSelectPage = new UserSelectPage(
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
page
)
await use(userSelectPage)
}
})

View File

@@ -43,7 +43,7 @@ export class ComfyNodeSearchBox {
}
get filterButton() {
return this.page.locator('.comfy-vue-node-search-container .filter-button')
return this.page.locator('.comfy-vue-node-search-container ._filter-button')
}
async fillAndSelectFirstNode(

View File

@@ -1,40 +0,0 @@
import { Page } from '@playwright/test'
export class SettingDialog {
constructor(public readonly page: Page) {}
async open() {
const button = this.page.locator('button.comfy-settings-btn:visible')
await button.click()
await this.page.waitForSelector('div.settings-container')
}
/**
* Set the value of a text setting
* @param id - The id of the setting
* @param value - The value to set
*/
async setStringSetting(id: string, value: string) {
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
await settingInputDiv.locator('input').fill(value)
}
/**
* Toggle the value of a boolean setting
* @param id - The id of the setting
*/
async toggleBooleanSetting(id: string) {
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
await settingInputDiv.locator('input').click()
}
async goToAboutPanel() {
const aboutButton = this.page.locator('li[aria-label="About"]')
await aboutButton.click()
await this.page.waitForSelector('div.about-container')
}
}

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test'
import { Page } from '@playwright/test'
class SidebarTab {
constructor(
@@ -85,65 +85,36 @@ 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
.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()
}
async switchToWorkflow(workflowName: string) {
const workflowLocator = this.getOpenedItem(workflowName)
const workflowLocator = this.page.locator(
'.comfyui-workflows-open .node-label',
{ hasText: workflowName }
)
await workflowLocator.click()
await this.page.waitForTimeout(300)
}
getOpenedItem(name: string) {
return this.root.locator('.comfyui-workflows-open .node-label', {
hasText: name
})
}
getPersistedItem(name: string) {
return this.root.locator('.comfyui-workflows-browse .node-label', {
hasText: name
})
}
async renameWorkflow(locator: Locator, newName: string) {
await locator.click({ button: 'right' })
await this.page
.locator('.p-contextmenu-item-content', { hasText: 'Rename' })
.click()
await this.page.keyboard.type(newName)
await this.page.keyboard.press('Enter')
await this.page.waitForTimeout(300)
}
}

View File

@@ -13,53 +13,27 @@ export class Topbar {
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
}
getMenuItem(itemLabel: string): Locator {
async getMenuItem(itemLabel: string): Promise<Locator> {
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
}
getWorkflowTab(tabName: string): Locator {
async getWorkflowTab(tabName: string): Promise<Locator> {
return this.page
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
.locator('..')
}
async closeWorkflowTab(tabName: string) {
const tab = this.getWorkflowTab(tabName)
const tab = await this.getWorkflowTab(tabName)
await tab.locator('.close-button').click({ force: true })
}
getSaveDialog(): Locator {
return this.page.locator('.p-dialog-content input')
}
saveWorkflow(workflowName: string): Promise<void> {
return this._saveWorkflow(workflowName, 'Save')
}
saveWorkflowAs(workflowName: string): Promise<void> {
return this._saveWorkflow(workflowName, 'Save As')
}
exportWorkflow(workflowName: string): Promise<void> {
return this._saveWorkflow(workflowName, 'Export')
}
async _saveWorkflow(
workflowName: string,
command: 'Save' | 'Save As' | 'Export'
) {
await this.triggerTopbarCommand(['Workflow', command])
await this.getSaveDialog().fill(workflowName)
async saveWorkflow(workflowName: string) {
await this.triggerTopbarCommand(['Workflow', 'Save'])
await this.page.locator('.p-dialog-content input').fill(workflowName)
await this.page.keyboard.press('Enter')
// Wait for workflow service to finish saving
await this.page.waitForFunction(
() => !window['app'].extensionManager.workflow.isBusy,
undefined,
{ timeout: 3000 }
)
// Wait for the dialog to close.
await this.getSaveDialog().waitFor({ state: 'hidden', timeout: 500 })
await this.page.waitForTimeout(300)
}
async triggerTopbarCommand(path: string[]) {

View File

@@ -233,10 +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.promptDialogInput.fill(groupNodeName)
await this.comfyPage.page.keyboard.press('Enter')
await this.comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await this.comfyPage.nextFrame()
const nodes = await this.comfyPage.getNodeRefsByType(
`workflow>${groupNodeName}`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -77,13 +77,8 @@ test.describe('Group Node', () => {
.click()
})
})
// The 500ms fixed delay on the search results is causing flakiness
// Potential solution: add a spinner state when the search is in progress,
// and observe that state from the test. Blocker: the PrimeVue AutoComplete
// does not have a v-model on the query, so we cannot observe the raw
// query update, and thus cannot set the spinning state between the raw query
// update and the debounced search update.
test.skip('Can be added to canvas using search', async ({ comfyPage }) => {
test('Can be added to canvas using search', async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas()
@@ -103,36 +98,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
}) => {
@@ -174,9 +139,7 @@ test.describe('Group Node', () => {
}) => {
await comfyPage.loadWorkflow('legacy_group_node')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
).not.toBeVisible()
expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
})
test.describe('Copy and paste', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,14 +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) {
@@ -25,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)

View File

@@ -1,27 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
await comfyPage.canvas.press('Delete')
await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
})
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('pinned-all.png')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('unpinned-all.png')
})
})
test.describe('Node Interaction', () => {
test('Can enter prompt', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
@@ -33,7 +12,7 @@ test.describe('Node Interaction', () => {
})
test.describe('Node Selection', () => {
const multiSelectModifiers = ['Control', 'Shift', 'Meta'] as const
const multiSelectModifiers = ['Control', 'Shift', 'Meta']
multiSelectModifiers.forEach((modifier) => {
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
@@ -137,24 +116,6 @@ test.describe('Node Interaction', () => {
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
})
test('Auto snap&highlight when dragging link over node', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true)
await comfyPage.page.mouse.move(
comfyPage.clipTextEncodeNode1InputSlot.x,
comfyPage.clipTextEncodeNode1InputSlot.y
)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(
comfyPage.clipTextEncodeNode2InputSlot.x,
comfyPage.clipTextEncodeNode2InputSlot.y
)
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
})
})
test('Can adjust widget value', async ({ comfyPage }) => {
@@ -285,8 +246,7 @@ test.describe('Node Interaction', () => {
position: {
x: 50,
y: 10
},
delay: 5
}
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
@@ -301,8 +261,7 @@ test.describe('Node Interaction', () => {
position: {
x: 50,
y: 50
},
delay: 5
}
})
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
})
@@ -320,15 +279,6 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
})
test('Can fit group to contents', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
})
// Somehow this test fails on GitHub Actions. It works locally.
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
@@ -363,8 +313,7 @@ test.describe('Group Interaction', () => {
position: {
x: 50,
y: 10
},
delay: 5
}
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
@@ -516,7 +465,7 @@ test.describe('Widget Interaction', () => {
await expect(textBox).toHaveValue('')
await textBox.fill('Hello World')
await expect(textBox).toHaveValue('Hello World')
await comfyPage.ctrlZ(null)
await comfyPage.ctrlZ()
await expect(textBox).toHaveValue('')
})
@@ -527,9 +476,9 @@ test.describe('Widget Interaction', () => {
await textBox.fill('1girl')
await expect(textBox).toHaveValue('1girl')
await textBox.selectText()
await comfyPage.ctrlArrowUp(null)
await comfyPage.ctrlArrowUp()
await expect(textBox).toHaveValue('(1girl:1.05)')
await comfyPage.ctrlZ(null)
await comfyPage.ctrlZ()
await expect(textBox).toHaveValue('1girl')
})
})
@@ -539,41 +488,6 @@ test.describe('Load workflow', () => {
await comfyPage.loadWorkflow('string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
})
test('Can load workflow with ("STRING",) input node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
})
test('Restore workflow on reload (switch workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.reload({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
test('Restore workflow on reload (modify workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getFirstNodeRef())!
await node.click('collapse')
// Wait 300ms between 2 clicks so that it is not treated as a double click
// by litegraph.
await comfyPage.page.waitForTimeout(300)
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
await comfyPage.reload({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
})
})
test.describe('Load duplicate workflow', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

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