fix: merge main, fix Segformer node class name per CodeRabbit review

- Resolve merge conflicts with main
- Fix incorrect Segformer node class: LS_LoadSegformerModel -> LoadSegformerModel
  (LS_LoadSegformerModel is only the internal node ID, not the Python class name)
- Update tests to use correct class name

Amp-Thread-ID: https://ampcode.com/threads/T-019c0cdc-0150-7698-b71f-1f18d053521c
This commit is contained in:
bymyself
2026-01-31 20:20:07 -08:00
372 changed files with 11308 additions and 4299 deletions

1
.gitattributes vendored
View File

@@ -11,6 +11,7 @@
*.ts text eol=lf *.ts text eol=lf
*.vue text eol=lf *.vue text eol=lf
*.yaml text eol=lf *.yaml text eol=lf
*.yml text eol=lf
# Generated files # Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -104,14 +104,14 @@ runs:
- name: Find existing comment - name: Find existing comment
id: find id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
with: with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }} issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot] comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }} body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment - name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with: with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }} issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }} comment-id: ${{ steps.find.outputs.comment-id }}

View File

@@ -16,7 +16,7 @@ runs:
# Checkout ComfyUI repo, install the dev_tools node and start server # Checkout ComfyUI repo, install the dev_tools node and start server
- name: Checkout ComfyUI - name: Checkout ComfyUI
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
repository: 'comfyanonymous/ComfyUI' repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI' path: 'ComfyUI'
@@ -33,7 +33,7 @@ runs:
fi fi
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: '3.10' python-version: '3.10'

View File

@@ -12,29 +12,17 @@ runs:
# Install pnpm, Node.js, build frontend # Install pnpm, Node.js, build frontend
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml' cache-dependency-path: './pnpm-lock.yaml'
# Restore tool caches before running any build/lint operations
- name: Restore tool output cache
uses: actions/cache/restore@v4
with:
path: |
./.cache
./tsconfig.tsbuildinfo
key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }}
restore-keys: |
tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-
tool-cache-${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -11,7 +11,7 @@ runs:
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
- name: Cache Playwright Browsers - name: Cache Playwright Browsers
uses: actions/cache@v4 uses: actions/cache@v5 # v5.0.2
id: cache-playwright-browsers id: cache-playwright-browsers
with: with:
path: '~/.cache/ms-playwright' path: '~/.cache/ms-playwright'

View File

@@ -13,15 +13,15 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: lts/* node-version: lts/*
cache: 'pnpm' cache: 'pnpm'
@@ -36,7 +36,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}' commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'

View File

@@ -18,15 +18,15 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: lts/* node-version: lts/*
cache: 'pnpm' cache: 'pnpm'
@@ -35,7 +35,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Checkout ComfyUI-Manager repository - name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
repository: Comfy-Org/ComfyUI-Manager repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager path: ComfyUI-Manager
@@ -86,7 +86,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true' if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}' commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'

View File

@@ -17,15 +17,15 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: lts/* node-version: lts/*
cache: 'pnpm' cache: 'pnpm'
@@ -34,7 +34,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Checkout comfy-api repository - name: Checkout comfy-api repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
repository: Comfy-Org/comfy-api repository: Comfy-Org/comfy-api
path: comfy-api path: comfy-api
@@ -87,7 +87,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true' if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}' commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'

View File

@@ -13,6 +13,6 @@ jobs:
json-lint: json-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Validate JSON syntax - name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh run: ./scripts/cicd/check-json.sh

View File

@@ -18,23 +18,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }} ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
token: ${{ secrets.PR_GH_TOKEN }}
- name: Install pnpm - name: Setup frontend
uses: pnpm/action-setup@v4 uses: ./.github/actions/setup-frontend
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint with auto-fix - name: Run ESLint with auto-fix
run: pnpm lint:fix run: pnpm lint:fix
@@ -73,7 +63,7 @@ jobs:
- name: Comment on PR about auto-fix - name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true continue-on-error: true
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
@@ -86,7 +76,7 @@ jobs:
- name: Comment on PR about manual fix needed - name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true continue-on-error: true
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({

View File

@@ -16,10 +16,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: '3.11' python-version: '3.11'

View File

@@ -17,21 +17,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Install pnpm - name: Setup frontend
uses: pnpm/action-setup@v4.1.0 uses: ./.github/actions/setup-frontend
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build project - name: Build project
run: pnpm build run: pnpm build
@@ -46,7 +35,7 @@ jobs:
echo ${{ github.base_ref }} > ./temp/size/base.txt echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload size data - name: Upload size data
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: size-data name: size-data
path: temp/size path: temp/size

View File

@@ -31,11 +31,11 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Get PR Number - name: Get PR Number
id: pr id: pr
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
const { data: prs } = await github.rest.pulls.list({ const { data: prs } = await github.rest.pulls.list({
@@ -68,7 +68,7 @@ jobs:
- name: Download and Deploy Reports - name: Download and Deploy Reports
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}

View File

@@ -5,8 +5,8 @@ on:
push: push:
branches: [main, master, core/*, desktop/*] branches: [main, master, core/*, desktop/*]
pull_request: pull_request:
branches-ignore: branches-ignore: [wip/*, draft/*, temp/*]
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*] workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup frontend - name: Setup frontend
uses: ./.github/actions/setup-frontend uses: ./.github/actions/setup-frontend
with: with:
@@ -25,7 +25,7 @@ jobs:
# Upload only built dist/ (containerized test jobs will pnpm install without cache) # Upload only built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend - name: Upload built frontend
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: frontend-dist name: frontend-dist
path: dist/ path: dist/
@@ -51,9 +51,9 @@ jobs:
shardTotal: [8] shardTotal: [8]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Download built frontend - name: Download built frontend
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: frontend-dist name: frontend-dist
path: dist/ path: dist/
@@ -72,7 +72,7 @@ jobs:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
- name: Upload blob report - name: Upload blob report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
name: blob-report-chromium-${{ matrix.shardIndex }} name: blob-report-chromium-${{ matrix.shardIndex }}
@@ -98,9 +98,9 @@ jobs:
browser: [chromium-2x, chromium-0.5x, mobile-chrome] browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Download built frontend - name: Download built frontend
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: frontend-dist name: frontend-dist
path: dist/ path: dist/
@@ -128,7 +128,7 @@ jobs:
pnpm exec playwright merge-reports --reporter=json ./blob-report pnpm exec playwright merge-reports --reporter=json ./blob-report
- name: Upload Playwright report - name: Upload Playwright report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
if: always() if: always()
with: with:
name: playwright-report-${{ matrix.browser }} name: playwright-report-${{ matrix.browser }}
@@ -141,16 +141,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
steps: steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Download blob reports - name: Download blob reports
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
path: ./all-blob-reports path: ./all-blob-reports
pattern: blob-report-chromium-* pattern: blob-report-chromium-*
@@ -165,7 +162,7 @@ jobs:
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
- name: Upload HTML report - name: Upload HTML report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: playwright-report-chromium name: playwright-report-chromium
path: ./playwright-report/ path: ./playwright-report/
@@ -183,7 +180,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Get start time - name: Get start time
id: start-time id: start-time
@@ -210,10 +207,10 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Download all playwright reports - name: Download all playwright reports
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
pattern: playwright-report-* pattern: playwright-report-*
path: reports path: reports

View File

@@ -31,11 +31,11 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Get PR Number - name: Get PR Number
id: pr id: pr
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
const { data: prs } = await github.rest.pulls.list({ const { data: prs } = await github.rest.pulls.list({
@@ -68,7 +68,7 @@ jobs:
- name: Download and Deploy Storybook - name: Download and Deploy Storybook
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Post starting comment - name: Post starting comment
env: env:
@@ -36,21 +36,10 @@ jobs:
workflow-url: ${{ steps.workflow-url.outputs.url }} workflow-url: ${{ steps.workflow-url.outputs.url }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install pnpm - name: Setup frontend
uses: pnpm/action-setup@v4 uses: ./.github/actions/setup-frontend
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Storybook - name: Build Storybook
run: pnpm build-storybook run: pnpm build-storybook
@@ -69,7 +58,7 @@ jobs:
- name: Upload Storybook build - name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: storybook-static name: storybook-static
path: storybook-static/ path: storybook-static/
@@ -86,27 +75,16 @@ jobs:
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }} chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Required for Chromatic baseline fetch-depth: 0 # Required for Chromatic baseline
- name: Install pnpm - name: Setup frontend
uses: pnpm/action-setup@v4 uses: ./.github/actions/setup-frontend
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Storybook and run Chromatic - name: Build Storybook and run Chromatic
id: chromatic id: chromatic
uses: chromaui/action@latest uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
with: with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook buildScriptName: build-storybook
@@ -136,11 +114,11 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Download Storybook build - name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success' if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: storybook-static name: storybook-static
path: storybook-static path: storybook-static
@@ -170,7 +148,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Update comment with Chromatic URLs - name: Update comment with Chromatic URLs
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}'; const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';

View File

@@ -16,21 +16,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Install pnpm - name: Setup frontend
uses: pnpm/action-setup@v4 uses: ./.github/actions/setup-frontend
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Vitest tests - name: Run Vitest tests
run: pnpm test:unit run: pnpm test:unit

View File

@@ -0,0 +1,21 @@
name: Validate Action SHA Pins
on:
pull_request:
paths:
- '.github/workflows/**'
- '.github/actions/**'
- '.pinact.yaml'
permissions:
contents: read
jobs:
validate-pins:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: suzuki-shunsuke/pinact-action@3d49c6412901042473ffa78becddab1aea46bbea # v1.3.1
with:
skip_push: 'true'

View File

@@ -17,10 +17,10 @@ jobs:
yaml-lint: yaml-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: '3.x' python-version: '3.x'

View File

@@ -18,12 +18,12 @@ jobs:
steps: steps:
- name: Checkout merge commit - name: Checkout merge commit
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.merge_commit_sha }} ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'

View File

@@ -16,7 +16,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with:
token: ${{ secrets.PR_GH_TOKEN }}
# Setup playwright environment # Setup playwright environment
- name: Setup ComfyUI Frontend - name: Setup ComfyUI Frontend

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
# Setup playwright environment with custom node repository # Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching) - name: Setup ComfyUI Server (without launching)
@@ -36,7 +36,7 @@ jobs:
# Install the custom node repository # Install the custom node repository
- name: Checkout custom node repository - name: Checkout custom node repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
repository: ${{ inputs.owner }}/${{ inputs.repository }} repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}' path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
@@ -113,7 +113,7 @@ jobs:
git commit -m "Update locales" git commit -m "Update locales"
- name: Install SSH key For PUSH - name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
with: with:
# PR private key from action server # PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }} key: ${{ secrets.PR_SSH_PRIVATE_KEY }}

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
# Setup playwright environment # Setup playwright environment
- name: Setup ComfyUI Server (and start) - name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server uses: ./.github/actions/setup-comfyui-server
@@ -40,7 +40,7 @@ jobs:
env: env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'Update locales for node definitions' commit-message: 'Update locales for node definitions'

View File

@@ -64,7 +64,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -23,18 +23,18 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/head ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: '20'
cache: 'pnpm' cache: 'pnpm'
@@ -44,7 +44,7 @@ jobs:
pnpm install -g typescript @vue/compiler-sfc pnpm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review - name: Run Claude PR Review
uses: anthropics/claude-code-action@v1.0.6 uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with: with:
label_trigger: 'claude-review' label_trigger: 'claude-review'
prompt: | prompt: |

View File

@@ -33,24 +33,13 @@ jobs:
github.event_name == 'workflow_dispatch' github.event_name == 'workflow_dispatch'
) )
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Install pnpm - name: Setup frontend
uses: pnpm/action-setup@v4.1.0 uses: ./.github/actions/setup-frontend
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Download size data - name: Download size data
uses: dawidd6/action-download-artifact@v11 uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with: with:
name: size-data name: size-data
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }} run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
@@ -75,7 +64,7 @@ jobs:
fi fi
- name: Download previous size data - name: Download previous size data
uses: dawidd6/action-download-artifact@v11 uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with: with:
branch: ${{ steps.pr-base.outputs.content }} branch: ${{ steps.pr-base.outputs.content }}
workflow: ci-size-data.yaml workflow: ci-size-data.yaml
@@ -89,12 +78,12 @@ jobs:
- name: Read size report - name: Read size report
id: size-report id: size-report
uses: juliangruber/read-file-action@v1 uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with: with:
path: ./size-report.md path: ./size-report.md
- name: Create or update PR comment - name: Create or update PR comment
uses: actions-cool/maintain-one-comment@v3 uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }} number: ${{ steps.pr-number.outputs.content }}

View File

@@ -38,7 +38,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find Update Comment - name: Find Update Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: 'find-update-comment' id: 'find-update-comment'
with: with:
issue-number: ${{ steps.pr-info.outputs.pr-number }} issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -46,7 +46,7 @@ jobs:
body-includes: 'Updating Playwright Expectations' body-includes: 'Updating Playwright Expectations'
- name: Add Starting Reaction - name: Add Starting Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with: with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }} comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-info.outputs.pr-number }} issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -56,7 +56,7 @@ jobs:
reactions: eyes reactions: eyes
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ steps.pr-info.outputs.branch }} ref: ${{ steps.pr-info.outputs.branch }}
- name: Setup frontend - name: Setup frontend
@@ -66,7 +66,7 @@ jobs:
# Upload built dist/ (containerized test jobs will pnpm install without cache) # Upload built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend - name: Upload built frontend
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: frontend-dist name: frontend-dist
path: dist/ path: dist/
@@ -91,11 +91,11 @@ jobs:
shardTotal: [4] shardTotal: [4]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ needs.setup.outputs.branch }} ref: ${{ needs.setup.outputs.branch }}
- name: Download built frontend - name: Download built frontend
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: frontend-dist name: frontend-dist
path: dist/ path: dist/
@@ -149,7 +149,7 @@ jobs:
# Upload ONLY the changed files from this shard # Upload ONLY the changed files from this shard
- name: Upload changed snapshots - name: Upload changed snapshots
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
if: steps.changed-snapshots.outputs.has-changes == 'true' if: steps.changed-snapshots.outputs.has-changes == 'true'
with: with:
name: snapshots-shard-${{ matrix.shardIndex }} name: snapshots-shard-${{ matrix.shardIndex }}
@@ -157,7 +157,7 @@ jobs:
retention-days: 1 retention-days: 1
- name: Upload test report - name: Upload test report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
if: always() if: always()
with: with:
name: playwright-report-shard-${{ matrix.shardIndex }} name: playwright-report-shard-${{ matrix.shardIndex }}
@@ -170,17 +170,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ needs.setup.outputs.branch }} ref: ${{ needs.setup.outputs.branch }}
# Download all changed snapshot files from shards # Download all changed snapshot files from shards
- name: Download snapshot artifacts - name: Download snapshot artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
pattern: snapshots-shard-* pattern: snapshots-shard-*
path: ./downloaded-snapshots path: ./downloaded-snapshots
merge-multiple: false merge-multiple: true
- name: List downloaded files - name: List downloaded files
run: | run: |
@@ -206,13 +206,13 @@ jobs:
echo "MERGING CHANGED SNAPSHOTS" echo "MERGING CHANGED SNAPSHOTS"
echo "==========================================" echo "=========================================="
# Check if any artifacts were downloaded # Check if any artifacts were downloaded (merge-multiple puts files directly in path)
if [ ! -d "./downloaded-snapshots" ]; then if [ ! -d "./downloaded-snapshots" ]; then
echo "No snapshot artifacts to merge" echo "No snapshot artifacts to merge"
echo "==========================================" echo "=========================================="
echo "MERGE COMPLETE" echo "MERGE COMPLETE"
echo "==========================================" echo "=========================================="
echo "Shards merged: 0" echo "Files merged: 0"
exit 0 exit 0
fi fi
@@ -222,37 +222,29 @@ jobs:
exit 1 exit 1
fi fi
merged_count=0 # Count files to merge
file_count=$(find ./downloaded-snapshots -type f | wc -l)
# For each shard's changed files, copy them directly if [ "$file_count" -eq 0 ]; then
for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do echo "No snapshot files found in downloaded artifacts"
if [ ! -d "$shard_dir" ]; then echo "=========================================="
continue echo "MERGE COMPLETE"
fi echo "=========================================="
echo "Files merged: 0"
exit 0
fi
shard_name=$(basename "$shard_dir") echo "Merging $file_count snapshot file(s)..."
file_count=$(find "$shard_dir" -type f | wc -l)
if [ "$file_count" -eq 0 ]; then # Copy all files directly, preserving directory structure
echo " $shard_name: no files" # With merge-multiple: true, files are directly in ./downloaded-snapshots/ without shard subdirs
continue cp -v -r ./downloaded-snapshots/* browser_tests/ 2>&1 | sed 's/^/ /'
fi
echo "Processing $shard_name ($file_count file(s))..."
# Copy files directly, preserving directory structure
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
merged_count=$((merged_count + 1))
echo " ✓ Merged"
echo ""
done
echo ""
echo "==========================================" echo "=========================================="
echo "MERGE COMPLETE" echo "MERGE COMPLETE"
echo "==========================================" echo "=========================================="
echo "Shards merged: $merged_count" echo "Files merged: $file_count"
- name: Show changes - name: Show changes
run: | run: |
@@ -301,7 +293,7 @@ jobs:
echo "✓ Commit and push successful" echo "✓ Commit and push successful"
- name: Add Done Reaction - name: Add Done Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true' if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true'
with: with:
comment-id: ${{ needs.setup.outputs.comment-id }} comment-id: ${{ needs.setup.outputs.comment-id }}

View File

@@ -20,13 +20,13 @@ jobs:
dist_tag: ${{ steps.dist.outputs.dist_tag }} dist_tag: ${{ steps.dist.outputs.dist_tag }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.merge_commit_sha }} ref: ${{ github.event.pull_request.merge_commit_sha }}
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: '24.x' node-version: '24.x'
@@ -71,7 +71,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout merge commit - name: Checkout merge commit
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.merge_commit_sha }} ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2 fetch-depth: 2

View File

@@ -77,19 +77,19 @@ jobs:
fi fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ steps.resolve_ref.outputs.ref }} ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1 fetch-depth: 1
persist-credentials: false persist-credentials: false
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: '24.x' node-version: '24.x'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -61,13 +61,13 @@ jobs:
steps: steps:
- name: Checkout ComfyUI_frontend - name: Checkout ComfyUI_frontend
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
path: frontend path: frontend
- name: Checkout ComfyUI (sparse) - name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
repository: Comfy-Org/ComfyUI repository: Comfy-Org/ComfyUI
sparse-checkout: | sparse-checkout: |
@@ -75,12 +75,12 @@ jobs:
path: comfyui path: comfyui
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: lts/* node-version: lts/*
@@ -169,7 +169,7 @@ jobs:
steps: steps:
- name: Checkout ComfyUI fork - name: Checkout ComfyUI fork
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }} repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}

View File

@@ -18,13 +18,13 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }} token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: 'lts/*'

View File

@@ -19,12 +19,12 @@ jobs:
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'pnpm' cache: 'pnpm'
@@ -50,12 +50,13 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true' ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true' USE_PROD_CONFIG: 'true'
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
run: | run: |
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm build pnpm build
pnpm zipdist pnpm zipdist
- name: Upload dist artifact - name: Upload dist artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: dist-files name: dist-files
path: | path: |
@@ -66,16 +67,13 @@ jobs:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download dist artifact - name: Download dist artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: dist-files name: dist-files
- name: Create release - name: Create release
id: create_release id: create_release
uses: >- uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -98,13 +96,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Download dist artifact - name: Download dist artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: dist-files name: dist-files
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install build dependencies - name: Install build dependencies
@@ -119,8 +117,7 @@ jobs:
env: env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }} COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package - name: Publish pypi package
uses: >- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
with: with:
password: ${{ secrets.PYPI_TOKEN }} password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist packages-dir: comfyui_frontend_package/dist
@@ -147,7 +144,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout merge commit - name: Checkout merge commit
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.merge_commit_sha }} ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2 fetch-depth: 2

View File

@@ -69,18 +69,18 @@ jobs:
fi fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ steps.resolve_ref.outputs.ref }} ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1 fetch-depth: 1
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -15,12 +15,12 @@ jobs:
version: ${{ steps.current_version.outputs.version }} version: ${{ steps.current_version.outputs.version }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'pnpm' cache: 'pnpm'
@@ -40,7 +40,7 @@ jobs:
pnpm build pnpm build
pnpm zipdist pnpm zipdist
- name: Upload dist artifact - name: Upload dist artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: dist-files name: dist-files
path: | path: |
@@ -52,13 +52,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Download dist artifact - name: Download dist artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: dist-files name: dist-files
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install build dependencies - name: Install build dependencies
@@ -73,7 +73,7 @@ jobs:
env: env:
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }} COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
- name: Publish pypi package - name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with: with:
password: ${{ secrets.PYPI_TOKEN }} password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist packages-dir: comfyui_frontend_package/dist

View File

@@ -65,7 +65,7 @@ jobs:
- name: Close stale nightly version bump PRs - name: Close stale nightly version bump PRs
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
script: | script: |
@@ -118,7 +118,7 @@ jobs:
core.info(`Closed ${closed.length} stale PR(s).`) core.info(`Closed ${closed.length} stale PR(s).`)
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ steps.prepared-inputs.outputs.branch }} ref: ${{ steps.prepared-inputs.outputs.branch }}
fetch-depth: 0 fetch-depth: 0
@@ -142,12 +142,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists" echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: lts/* node-version: lts/*
@@ -180,7 +180,7 @@ jobs:
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT" echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}' commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.inputs.branch }} ref: ${{ github.event.inputs.branch }}
fetch-depth: 0 fetch-depth: 0
@@ -51,12 +51,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists" echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: '24.x' node-version: '24.x'
cache: 'pnpm' cache: 'pnpm'
@@ -79,7 +79,7 @@ jobs:
echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}' commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -22,18 +22,18 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 50
ref: main ref: main
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with: with:
version: 10 version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: '20'
cache: 'pnpm' cache: 'pnpm'
@@ -49,7 +49,7 @@ jobs:
fi fi
- name: Run Claude Documentation Review - name: Run Claude Documentation Review
uses: anthropics/claude-code-action@v1.0.6 uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with: with:
prompt: | prompt: |
Is all documentation still 100% accurate? Is all documentation still 100% accurate?
@@ -130,7 +130,7 @@ jobs:
- name: Create or Update Pull Request - name: Create or Update Pull Request
if: steps.check_changes.outputs.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.PR_GH_TOKEN }} token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'docs: weekly documentation accuracy update' commit-message: 'docs: weekly documentation accuracy update'

24
.pinact.yaml Normal file
View File

@@ -0,0 +1,24 @@
# pinact configuration
# https://github.com/suzuki-shunsuke/pinact
version: 3
files:
- pattern: .github/workflows/*.yaml
- pattern: .github/actions/**/*.yaml
# Actions that don't need SHA pinning (official GitHub actions are trusted)
ignore_actions:
- name: actions/cache
ref: v5
- name: actions/checkout
ref: v6
- name: actions/setup-node
ref: v6
- name: actions/setup-python
ref: v6
- name: actions/upload-artifact
ref: v6
- name: actions/download-artifact
ref: v7
- name: actions/github-script
ref: v8

View File

@@ -8,3 +8,6 @@ rules:
line-length: disable line-length: disable
document-start: disable document-start: disable
truthy: disable truthy: disable
comments:
min-spaces-from-content: 1

View File

@@ -5,7 +5,7 @@ import * as fs from 'fs'
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph' import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema' import type { KeyCombo } from '../../src/platform/keybindings'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore' import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource' import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar' import { ComfyActionbar } from '../helpers/actionbar'

View File

@@ -7,7 +7,7 @@ import { webSocketFixture } from '../fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture) const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Actionbar', () => { test.describe('Actionbar', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
}) })

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Bottom Panel Shortcuts', () => { test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
}) })

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Browser tab title', () => { test.describe('Browser tab title', { tag: '@smoke' }, () => {
test.describe('Beta Menu', () => { test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -19,7 +19,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Change Tracker', () => { test.describe('Change Tracker', { tag: '@workflow' }, () => {
test.describe('Undo/Redo', () => { test.describe('Undo/Redo', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -151,7 +151,7 @@ const customColorPalettes: Record<string, Palette> = {
} }
} }
test.describe('Color Palette', () => { test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
test('Can show custom color palette', async ({ comfyPage }) => { test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes) await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly // Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
@@ -194,104 +194,110 @@ test.describe('Color Palette', () => {
}) })
}) })
test.describe('Node Color Adjustments', () => { test.describe(
test.beforeEach(async ({ comfyPage }) => { 'Node Color Adjustments',
await comfyPage.loadWorkflow('nodes/every_node_color') { tag: ['@screenshot', '@settings'] },
}) () => {
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.2-arc-theme.png'
)
})
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
const parsed = await (
await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
})
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png')
})
test.describe('Context menu color adjustments', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
})
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.2-arc-theme.png'
)
})
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light') await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3) await comfyPage.nextFrame()
const node = await comfyPage.getFirstNodeRef() const parsed = await (
await node?.clickContextMenuOption('Colors') await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
}) })
test('should persist color adjustments when changing custom node colors', async ({ test('should lighten node colors when switching to light theme', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.page await comfyPage.setSetting('Comfy.ColorPalette', 'light')
.locator('.litemenu-entry.submenu span:has-text("red")') await comfyPage.nextFrame()
.click()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png' 'node-lightened-colors.png'
) )
}) })
test('should persist color adjustments when removing custom node color', async ({ test.describe('Context menu color adjustments', () => {
comfyPage test.beforeEach(async ({ comfyPage }) => {
}) => { await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.page await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
.locator('.litemenu-entry.submenu span:has-text("No color")') const node = await comfyPage.getFirstNodeRef()
.click() await node?.clickContextMenuOption('Colors')
await expect(comfyPage.canvas).toHaveScreenshot( })
'node-opacity-0.3-color-removed.png'
) test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png'
)
})
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-removed.png'
)
})
}) })
}) }
}) )

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Keybindings', () => { test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should execute command', async ({ comfyPage }) => { test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => { await comfyPage.registerCommand('TestCommand', () => {
window['foo'] = true window['foo'] = true

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Copy Paste', () => { test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Can copy and paste node', async ({ comfyPage }) => { test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode() await comfyPage.clickEmptyLatentNode()
await comfyPage.page.mouse.move(10, 10) await comfyPage.page.mouse.move(10, 10)

View File

@@ -22,7 +22,7 @@ async function verifyCustomIconSvg(iconElement: Locator) {
expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'") expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'")
} }
test.describe('Custom Icons', () => { test.describe('Custom Icons', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
}) })

View File

@@ -1,14 +1,14 @@
import type { Locator } from '@playwright/test' import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema' import type { Keybinding } from '../../src/platform/keybindings'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Load workflow warning', () => { test.describe('Load workflow warning', { tag: '@ui' }, () => {
test('Should display a warning when loading a workflow with missing nodes', async ({ test('Should display a warning when loading a workflow with missing nodes', async ({
comfyPage comfyPage
}) => { }) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('DOM Widget', () => { test.describe('DOM Widget', { tag: '@widget' }, () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/collapsed_multiline') await comfyPage.loadWorkflow('widgets/collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input') const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
@@ -29,12 +29,16 @@ test.describe('DOM Widget', () => {
await expect(lastMultiline).not.toBeVisible() await expect(lastMultiline).not.toBeVisible()
}) })
test('Position update when entering focus mode', async ({ comfyPage }) => { test(
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') 'Position update when entering focus mode',
await comfyPage.executeCommand('Workspace.ToggleFocusMode') { tag: '@screenshot' },
await comfyPage.nextFrame() async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
}) await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
}
)
// No DOM widget should be created by creation of interim LGraphNode objects. // No DOM widget should be created by creation of interim LGraphNode objects.
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => { test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {

View File

@@ -6,40 +6,48 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Execution', () => { test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
test('Report error on unconnected slot', async ({ comfyPage }) => { test(
await comfyPage.disconnectEdge() 'Report error on unconnected slot',
await comfyPage.clickEmptySpace() { tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
await comfyPage.executeCommand('Comfy.QueuePrompt') await comfyPage.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible() await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click() await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page.locator('.comfy-error-report').waitFor({ await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden' state: 'hidden'
}) })
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png' 'execution-error-unconnected-slot.png'
) )
}) }
)
}) })
test.describe('Execute to selected output nodes', () => { test.describe(
test('Execute to selected output nodes', async ({ comfyPage }) => { 'Execute to selected output nodes',
await comfyPage.loadWorkflow('execution/partial_execution') { tag: ['@smoke', '@workflow'] },
const input = await comfyPage.getNodeRefById(3) () => {
const output1 = await comfyPage.getNodeRefById(1) test('Execute to selected output nodes', async ({ comfyPage }) => {
const output2 = await comfyPage.getNodeRefById(4) await comfyPage.loadWorkflow('execution/partial_execution')
expect(await (await input.getWidget(0)).getValue()).toBe('foo') const input = await comfyPage.getNodeRefById(3)
expect(await (await output1.getWidget(0)).getValue()).toBe('') const output1 = await comfyPage.getNodeRefById(1)
expect(await (await output2.getWidget(0)).getValue()).toBe('') const output2 = await comfyPage.getNodeRefById(4)
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo') expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('') expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
}) await output1.click('title')
})
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
})
}
)

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Feature Flags', () => { test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
test('Client and server exchange feature flags on connection', async ({ test('Client and server exchange feature flags on connection', async ({
comfyPage comfyPage
}) => { }) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Graph', () => { test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
// Should be able to fix link input slot index after swap the input order // Should be able to fix link input slot index after swap the input order
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348 // Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
test('Fix link input slots', async ({ comfyPage }) => { test('Fix link input slots', async ({ comfyPage }) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Graph Canvas Menu', () => { test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
// Set link render mode to spline to make sure it's not affected by other tests' // Set link render mode to spline to make sure it's not affected by other tests'
// side effects. // side effects.
@@ -15,29 +15,33 @@ test.describe('Graph Canvas Menu', () => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
}) })
test('Can toggle link visibility', async ({ comfyPage }) => { test(
const button = comfyPage.page.getByTestId('toggle-link-visibility-button') 'Can toggle link visibility',
await button.click() { tag: '@screenshot' },
await comfyPage.nextFrame() async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot( const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
'canvas-with-hidden-links.png' await button.click()
) await comfyPage.nextFrame()
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => { await expect(comfyPage.canvas).toHaveScreenshot(
return window['LiteGraph'].HIDDEN_LINK 'canvas-with-hidden-links.png'
}) )
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe( const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
hiddenLinkRenderMode return window['LiteGraph'].HIDDEN_LINK
) })
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
await button.click() await button.click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png' 'canvas-with-visible-links.png'
) )
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe( expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode hiddenLinkRenderMode
) )
}) }
)
test('Toggle minimap button is clickable and has correct test id', async ({ test('Toggle minimap button is clickable and has correct test id', async ({
comfyPage comfyPage

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Group Node', () => { test.describe('Group Node', { tag: '@node' }, () => {
test.describe('Node library sidebar', () => { test.describe('Node library sidebar', () => {
const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow' const groupNodeCategory = 'group nodes>workflow'
@@ -89,16 +89,20 @@ test.describe('Group Node', () => {
// does not have a v-model on the query, so we cannot observe the raw // 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 // query update, and thus cannot set the spinning state between the raw query
// update and the debounced search update. // update and the debounced search update.
test.skip('Can be added to canvas using search', async ({ comfyPage }) => { test.skip(
const groupNodeName = 'DefautWorkflowGroupNode' 'Can be added to canvas using search',
await comfyPage.convertAllNodesToGroupNode(groupNodeName) { tag: '@screenshot' },
await comfyPage.doubleClickCanvas() async ({ comfyPage }) => {
await comfyPage.nextFrame() const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName) await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot( await comfyPage.doubleClickCanvas()
'group-node-copy-added-from-search.png' await comfyPage.nextFrame()
) await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
}) await expect(comfyPage.canvas).toHaveScreenshot(
'group-node-copy-added-from-search.png'
)
}
)
test('Displays tooltip on title hover', async ({ comfyPage }) => { test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.EnableTooltips', true)

View File

@@ -13,7 +13,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Item Interaction', () => { test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
test('Can select/delete all items', async ({ comfyPage }) => { test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items') await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a') await comfyPage.canvas.press('Control+a')
@@ -60,13 +60,17 @@ test.describe('Node Interaction', () => {
}) })
}) })
test('@2x Can highlight selected', async ({ comfyPage }) => { test(
await expect(comfyPage.canvas).toHaveScreenshot('default.png') '@2x Can highlight selected',
await comfyPage.clickTextEncodeNode1() { tag: '@screenshot' },
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png') async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode2() await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png') await comfyPage.clickTextEncodeNode1()
}) await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
}
)
const dragSelectNodes = async ( const dragSelectNodes = async (
comfyPage: ComfyPage, comfyPage: ComfyPage,
@@ -150,12 +154,12 @@ test.describe('Node Interaction', () => {
}) })
}) })
test('Can drag node', async ({ comfyPage }) => { test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.dragNode2() await comfyPage.dragNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png') await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
}) })
test.describe('Edge Interaction', () => { test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action') await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
@@ -222,12 +226,18 @@ test.describe('Node Interaction', () => {
}) })
}) })
test('Can adjust widget value', async ({ comfyPage }) => { test(
await comfyPage.adjustWidgetValue() 'Can adjust widget value',
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png') { tag: '@screenshot' },
}) async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await expect(comfyPage.canvas).toHaveScreenshot(
'adjusted-widget-value.png'
)
}
)
test('Link snap to slot', async ({ comfyPage }) => { test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/snap_to_slot') await comfyPage.loadWorkflow('links/snap_to_slot')
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png') await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
@@ -244,57 +254,67 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png') await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
}) })
test('Can batch move links by drag with shift', async ({ comfyPage }) => { test(
await comfyPage.loadWorkflow('links/batch_move_links') 'Can batch move links by drag with shift',
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png') { tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = { const outputSlot1Pos = {
x: 304, x: 304,
y: 127 y: 127
}
const outputSlot2Pos = {
x: 307,
y: 310
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
} }
const outputSlot2Pos = { )
x: 307,
y: 310 test(
'Can batch disconnect links with ctrl+alt+click',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const loadCheckpointClipSlotPos = {
x: 332,
y: 508
}
await comfyPage.canvas.click({
modifiers: ['Control', 'Alt'],
position: loadCheckpointClipSlotPos
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'batch-disconnect-links-disconnected.png'
)
} }
)
await comfyPage.page.keyboard.down('Shift') test(
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos) 'Can toggle dom widget node open/closed',
await comfyPage.page.keyboard.up('Shift') { tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot('default.png')
'batch_move_links_moved.png' await comfyPage.clickTextEncodeNodeToggler()
) await expect(comfyPage.canvas).toHaveScreenshot(
}) 'text-encode-toggled-off.png'
)
test('Can batch disconnect links with ctrl+alt+click', async ({ await comfyPage.delay(1000)
comfyPage await comfyPage.clickTextEncodeNodeToggler()
}) => { await expect(comfyPage.canvas).toHaveScreenshot(
const loadCheckpointClipSlotPos = { 'text-encode-toggled-back-open.png'
x: 332, )
y: 508
} }
await comfyPage.canvas.click({ )
modifiers: ['Control', 'Alt'],
position: loadCheckpointClipSlotPos
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'batch-disconnect-links-disconnected.png'
)
})
test('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png'
)
await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png'
)
})
test('Can close prompt dialog with canvas click (number widget)', async ({ test('Can close prompt dialog with canvas click (number widget)', async ({
comfyPage comfyPage
@@ -341,19 +361,23 @@ test.describe('Node Interaction', () => {
await expect(legacyPrompt).toBeHidden() await expect(legacyPrompt).toBeHidden()
}) })
test('Can double click node title to edit', async ({ comfyPage }) => { test(
await comfyPage.loadWorkflow('nodes/single_ksampler') 'Can double click node title to edit',
await comfyPage.canvas.dblclick({ { tag: '@screenshot' },
position: { async ({ comfyPage }) => {
x: 50, await comfyPage.loadWorkflow('nodes/single_ksampler')
y: 10 await comfyPage.canvas.dblclick({
}, position: {
delay: 5 x: 50,
}) y: 10
await comfyPage.page.keyboard.type('Hello World') },
await comfyPage.page.keyboard.press('Enter') delay: 5
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png') })
}) await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
}
)
test('Double click node body does not trigger edit', async ({ test('Double click node body does not trigger edit', async ({
comfyPage comfyPage
@@ -369,29 +393,41 @@ test.describe('Node Interaction', () => {
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0) expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
}) })
test('Can group selected nodes', async ({ comfyPage }) => { test(
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10) 'Can group selected nodes',
await comfyPage.select2Nodes() { tag: '@screenshot' },
await comfyPage.page.keyboard.down('Control') async ({ comfyPage }) => {
await comfyPage.page.keyboard.press('KeyG') await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.page.keyboard.up('Control') await comfyPage.select2Nodes()
await comfyPage.nextFrame() await comfyPage.page.keyboard.down('Control')
// Confirm group title await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.press('Enter') await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png') // Confirm group title
}) await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-selected-nodes.png'
)
}
)
test('Can fit group to contents', async ({ comfyPage }) => { test(
await comfyPage.loadWorkflow('groups/oversized_group') 'Can fit group to contents',
await comfyPage.ctrlA() { tag: '@screenshot' },
await comfyPage.nextFrame() async ({ comfyPage }) => {
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.nextFrame() await comfyPage.ctrlA()
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png') await comfyPage.nextFrame()
}) await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-fit-to-contents.png'
)
}
)
test('Can pin/unpin nodes', async ({ comfyPage }) => { test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.select2Nodes() await comfyPage.select2Nodes()
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -401,20 +437,22 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png') await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
}) })
test('Can bypass/unbypass nodes with keyboard shortcut', async ({ test(
comfyPage 'Can bypass/unbypass nodes with keyboard shortcut',
}) => { { tag: '@screenshot' },
await comfyPage.select2Nodes() async ({ comfyPage }) => {
await comfyPage.canvas.press('Control+b') await comfyPage.select2Nodes()
await comfyPage.nextFrame() await comfyPage.canvas.press('Control+b')
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png') await comfyPage.nextFrame()
await comfyPage.canvas.press('Control+b') await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
await comfyPage.nextFrame() await comfyPage.canvas.press('Control+b')
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png') await comfyPage.nextFrame()
}) await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
}
)
}) })
test.describe('Group Interaction', () => { test.describe('Group Interaction', { tag: '@screenshot' }, () => {
test('Can double click group title to edit', async ({ comfyPage }) => { test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group') await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.canvas.dblclick({ await comfyPage.canvas.dblclick({
@@ -430,7 +468,7 @@ test.describe('Group Interaction', () => {
}) })
}) })
test.describe('Canvas Interaction', () => { test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test('Can zoom in/out', async ({ comfyPage }) => { test('Can zoom in/out', async ({ comfyPage }) => {
await comfyPage.zoom(-100) await comfyPage.zoom(-100)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png') await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
@@ -632,7 +670,7 @@ test.describe('Widget Interaction', () => {
}) })
}) })
test.describe('Load workflow', () => { test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Can load workflow with string node id', async ({ comfyPage }) => { test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/string_node_id') await comfyPage.loadWorkflow('nodes/string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png') await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
@@ -824,7 +862,7 @@ test.describe('Viewport settings', () => {
}) })
}) })
test.describe('Canvas Navigation', () => { test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Legacy Mode', () => { test.describe('Legacy Mode', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Keybindings', () => { test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should not trigger non-modifier keybinding when typing in input fields', async ({ test('Should not trigger non-modifier keybinding when typing in input fields', async ({
comfyPage comfyPage
}) => { }) => {

View File

@@ -14,7 +14,7 @@ function listenForEvent(): Promise<Event> {
}) })
} }
test.describe('Canvas Event', () => { test.describe('Canvas Event', { tag: '@canvas' }, () => {
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => { test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent) const eventPromise = comfyPage.page.evaluate(listenForEvent)
const disconnectPromise = comfyPage.disconnectEdge() const disconnectPromise = comfyPage.disconnectEdge()

View File

@@ -6,46 +6,50 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Load Workflow in Media', () => { test.describe(
const fileNames = [ 'Load Workflow in Media',
'workflow.webp', { tag: ['@screenshot', '@workflow'] },
'edited_workflow.webp', () => {
'no_workflow.webp', const fileNames = [
'large_workflow.webp', 'workflow.webp',
'workflow_prompt_parameters.png', 'edited_workflow.webp',
'workflow.webm', 'no_workflow.webp',
// Skipped due to 3d widget unstable visual result. 'large_workflow.webp',
// 3d widget shows grid after fully loaded. 'workflow_prompt_parameters.png',
// 'workflow.glb', 'workflow.webm',
'workflow.mp4', // Skipped due to 3d widget unstable visual result.
'workflow.mov', // 3d widget shows grid after fully loaded.
'workflow.m4v', // 'workflow.glb',
'workflow.svg' 'workflow.mp4',
// TODO: Re-enable after fixing test asset to use core nodes only 'workflow.mov',
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality 'workflow.m4v',
// 'workflow.avif' 'workflow.svg'
] // TODO: Re-enable after fixing test asset to use core nodes only
fileNames.forEach(async (fileName) => { // Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ // 'workflow.avif'
comfyPage ]
}) => { fileNames.forEach(async (fileName) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`) test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`) comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
}) })
})
const urls = [ const urls = [
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png' 'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
] ]
urls.forEach(async (url) => { urls.forEach(async (url) => {
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({ test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.dragAndDropURL(url) await comfyPage.dragAndDropURL(url)
const readableName = url.split('/').pop() const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png` `dropped_workflow_url_${readableName}.png`
) )
})
}) })
}) }
}) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('LOD Threshold', () => { test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
test('Should switch to low quality mode at correct zoom threshold', async ({ test('Should switch to low quality mode at correct zoom threshold', async ({
comfyPage comfyPage
}) => { }) => {
@@ -149,53 +149,55 @@ test.describe('LOD Threshold', () => {
expect(state.scale).toBeLessThan(0.2) // Very zoomed out expect(state.scale).toBeLessThan(0.2) // Very zoomed out
}) })
test('Should show visual difference between LOD on and off', async ({ test(
comfyPage 'Should show visual difference between LOD on and off',
}) => { { tag: '@screenshot' },
// Load a workflow with text-heavy nodes for clear visual difference async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default') // Load a workflow with text-heavy nodes for clear visual difference
await comfyPage.loadWorkflow('default')
// Set zoom level clearly below the threshold to ensure LOD activates // Set zoom level clearly below the threshold to ensure LOD activates
const targetZoom = 0.4 // Well below default threshold of ~0.571 const targetZoom = 0.4 // Well below default threshold of ~0.571
// Zoom to target level // Zoom to target level
await comfyPage.page.evaluate((zoom) => { await comfyPage.page.evaluate((zoom) => {
window['app'].canvas.ds.scale = zoom window['app'].canvas.ds.scale = zoom
window['app'].canvas.setDirty(true, true) window['app'].canvas.setDirty(true, true)
}, targetZoom) }, targetZoom)
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Take snapshot with LOD active (default 8px setting) // Take snapshot with LOD active (default 8px setting)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-low-quality.png' 'lod-comparison-low-quality.png'
) )
const lowQualityState = await comfyPage.page.evaluate(() => { const lowQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas const canvas = window['app'].canvas
return { return {
lowQuality: canvas.low_quality, lowQuality: canvas.low_quality,
scale: canvas.ds.scale scale: canvas.ds.scale
} }
}) })
expect(lowQualityState.lowQuality).toBe(true) expect(lowQualityState.lowQuality).toBe(true)
// Disable LOD to see high quality at same zoom // Disable LOD to see high quality at same zoom
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Take snapshot with LOD disabled (full quality at same zoom) // Take snapshot with LOD disabled (full quality at same zoom)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-high-quality.png' 'lod-comparison-high-quality.png'
) )
const highQualityState = await comfyPage.page.evaluate(() => { const highQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas const canvas = window['app'].canvas
return { return {
lowQuality: canvas.low_quality, lowQuality: canvas.low_quality,
scale: canvas.ds.scale scale: canvas.ds.scale
} }
}) })
expect(highQualityState.lowQuality).toBe(false) expect(highQualityState.lowQuality).toBe(false)
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2) expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
}) }
)
}) })

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Menu', () => { test.describe('Menu', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
}) })

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Minimap', () => { test.describe('Minimap', { tag: '@canvas' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', true) await comfyPage.setSetting('Comfy.Minimap.Visible', true)

View File

@@ -1,35 +1,39 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
test.describe('Mobile Baseline Snapshots', () => { test.describe(
test('@mobile empty canvas', async ({ comfyPage }) => { 'Mobile Baseline Snapshots',
await comfyPage.setSetting('Comfy.ConfirmClear', false) { tag: ['@mobile', '@screenshot'] },
await comfyPage.executeCommand('Comfy.ClearWorkflow') () => {
await expect(async () => { test('@mobile empty canvas', async ({ comfyPage }) => {
expect(await comfyPage.getGraphNodesCount()).toBe(0) await comfyPage.setSetting('Comfy.ConfirmClear', false)
}).toPass({ timeout: 256 }) await comfyPage.executeCommand('Comfy.ClearWorkflow')
await comfyPage.nextFrame() await expect(async () => {
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') expect(await comfyPage.getGraphNodesCount()).toBe(0)
}) }).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test('@mobile default workflow', async ({ comfyPage }) => { test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default') await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png' 'mobile-default-workflow.png'
) )
}) })
test('@mobile settings dialog', async ({ comfyPage }) => { test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open() await comfyPage.settingDialog.open()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.settingDialog.root).toHaveScreenshot( await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png', 'mobile-settings-dialog.png',
{ {
mask: [ mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator') comfyPage.settingDialog.root.getByTestId('current-user-indicator')
] ]
} }
) )
}) })
}) }
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Node Badge', () => { test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge', async ({ comfyPage }) => { test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge'] const LGraphBadge = window['LGraphBadge']
@@ -66,50 +66,60 @@ test.describe('Node Badge', () => {
}) })
}) })
test.describe('Node source badge', () => { test.describe(
Object.values(NodeBadgeMode).forEach(async (mode) => { 'Node source badge',
test(`Shows node badges (${mode})`, async ({ comfyPage }) => { { tag: ['@screenshot', '@smoke', '@node'] },
// Execution error workflow has both custom node and core node. () => {
await comfyPage.loadWorkflow('nodes/execution_error') Object.values(NodeBadgeMode).forEach(async (mode) => {
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode) test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode) // Execution error workflow has both custom node and core node.
await comfyPage.nextFrame() await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.resetView() await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await expect(comfyPage.canvas).toHaveScreenshot(`node-badge-${mode}.png`) await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()
await comfyPage.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(
`node-badge-${mode}.png`
)
})
}) })
}) }
}) )
test.describe('Node badge color', () => { test.describe(
test('Can show node badge with unknown color palette', async ({ 'Node badge color',
comfyPage { tag: ['@screenshot', '@smoke', '@node'] },
}) => { () => {
await comfyPage.setSetting( test('Can show node badge with unknown color palette', async ({
'Comfy.NodeBadge.NodeIdBadgeMode', comfyPage
NodeBadgeMode.ShowAll }) => {
) await comfyPage.setSetting(
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown') 'Comfy.NodeBadge.NodeIdBadgeMode',
await comfyPage.nextFrame() NodeBadgeMode.ShowAll
// Click empty space to trigger canvas re-render. )
await comfyPage.clickEmptySpace() await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
await expect(comfyPage.canvas).toHaveScreenshot( await comfyPage.nextFrame()
'node-badge-unknown-color-palette.png' // Click empty space to trigger canvas re-render.
) await comfyPage.clickEmptySpace()
}) await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
})
test('Can show node badge with light color palette', async ({ test('Can show node badge with light color palette', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting( await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode', 'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll NodeBadgeMode.ShowAll
) )
await comfyPage.setSetting('Comfy.ColorPalette', 'light') await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render. // Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace() await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png' 'node-badge-light-color-palette.png'
) )
}) })
}) }
)

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
// If an input is optional by node definition, it should be shown as // If an input is optional by node definition, it should be shown as
// a hollow circle no matter what shape it was defined in the workflow JSON. // a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', () => { test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
test('No shape specified', async ({ comfyPage }) => { test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_no_shape') await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')

View File

@@ -23,7 +23,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
await nodeRef.click('title') await nodeRef.click('title')
} }
test.describe('Node Help', () => { test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup() await comfyPage.setup()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Node search box', () => { test.describe('Node search box', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box') await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
@@ -46,14 +46,14 @@ test.describe('Node search box', () => {
await expect(comfyPage.searchBox.input).toBeVisible() await expect(comfyPage.searchBox.input).toBeVisible()
}) })
test('Can add node', async ({ comfyPage }) => { test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas() await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1) await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png') await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
}) })
test('Can auto link node', async ({ comfyPage }) => { test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.disconnectEdge() await comfyPage.disconnectEdge()
// Select the second item as the first item is always reroute // Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', { await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
@@ -62,41 +62,47 @@ test.describe('Node search box', () => {
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png') await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
}) })
test('Can auto link batch moved node', async ({ comfyPage }) => { test(
await comfyPage.loadWorkflow('links/batch_move_links') 'Can auto link batch moved node',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
const outputSlot1Pos = { const outputSlot1Pos = {
x: 304, x: 304,
y: 127 y: 127
}
const emptySpacePos = {
x: 5,
y: 5
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
} }
const emptySpacePos = { )
x: 5,
y: 5 test(
'Link release connecting to node with no slots',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
} }
await comfyPage.page.keyboard.down('Shift') )
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
})
test('Link release connecting to node with no slots', async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
})
test('Has correct aria-labels on search results', async ({ comfyPage }) => { test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint' const node = 'Load Checkpoint'
@@ -251,40 +257,45 @@ test.describe('Node search box', () => {
}) })
}) })
test.describe('Release context menu', () => { test.describe('Release context menu', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu') await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
}) })
test('Can trigger on link release', async ({ comfyPage }) => { test(
await comfyPage.disconnectEdge() 'Can trigger on link release',
const contextMenu = comfyPage.page.locator('.litecontextmenu') { tag: '@screenshot' },
// Wait for context menu with correct title (slot name | slot type) async ({ comfyPage }) => {
// The title shows the output slot name and type from the disconnected link await comfyPage.disconnectEdge()
await expect(contextMenu.locator('.litemenu-title')).toContainText( const contextMenu = comfyPage.page.locator('.litecontextmenu')
'CLIP | CLIP' // Wait for context menu with correct title (slot name | slot type)
) // The title shows the output slot name and type from the disconnected link
await comfyPage.page.mouse.move(10, 10) await expect(contextMenu.locator('.litemenu-title')).toContainText(
await comfyPage.nextFrame() 'CLIP | CLIP'
await expect(comfyPage.canvas).toHaveScreenshot( )
'link-release-context-menu.png' await comfyPage.page.mouse.move(10, 10)
) await comfyPage.nextFrame()
}) await expect(comfyPage.canvas).toHaveScreenshot(
'link-release-context-menu.png'
)
}
)
test('Can search and add node from context menu', async ({ test(
comfyPage, 'Can search and add node from context menu',
comfyMouse { tag: '@screenshot' },
}) => { async ({ comfyPage, comfyMouse }) => {
await comfyPage.disconnectEdge() await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 }) await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search') await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt') await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png' 'link-context-menu-search.png'
) )
}) }
)
test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({ test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({
comfyPage comfyPage

View File

@@ -6,8 +6,8 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Note Node', () => { test.describe('Note Node', { tag: '@node' }, () => {
test('Can load node nodes', async ({ comfyPage }) => { test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/note_nodes') await comfyPage.loadWorkflow('nodes/note_nodes')
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png') await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
}) })

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Primitive Node', () => { test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
test('Can load with correct size', async ({ comfyPage }) => { test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node') await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png') await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Record Audio Node', () => { test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
test('should add a record audio node and take a screenshot', async ({ test('should add a record audio node and take a screenshot', async ({
comfyPage comfyPage
}) => { }) => {

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Remote COMBO Widget', () => { test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
const mockOptions = ['d', 'c', 'b', 'a'] const mockOptions = ['d', 'c', 'b', 'a']
const addRemoteWidgetNode = async ( const addRemoteWidgetNode = async (

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils' import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
test.describe('Reroute Node', () => { test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
}) })
@@ -38,92 +38,96 @@ test.describe('Reroute Node', () => {
}) })
}) })
test.describe('LiteGraph Native Reroute Node', () => { test.describe(
test.beforeEach(async ({ comfyPage }) => { 'LiteGraph Native Reroute Node',
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') { tag: ['@screenshot', '@node'] },
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) () => {
}) test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test('loads from workflow', async ({ comfyPage }) => { test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute') await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png') await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
}) })
test('@2x @0.5x Can add reroute by alt clicking on link', async ({ test('@2x @0.5x Can add reroute by alt clicking on link', async ({
comfyPage comfyPage
}) => { }) => {
const loadCheckpointNode = ( const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint') await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0] )[0]
const clipEncodeNode = ( const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0] )[0]
const slot1 = await loadCheckpointNode.getOutput(1) const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0) const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint( const middlePoint = getMiddlePoint(
await slot1.getPosition(), await slot1.getPosition(),
await slot2.getPosition() await slot2.getPosition()
) )
await comfyPage.page.keyboard.down('Alt') await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt') await comfyPage.page.keyboard.up('Alt')
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_alt_click.png' 'native_reroute_alt_click.png'
) )
}) })
test('Can add reroute by clicking middle of link context menu', async ({ test('Can add reroute by clicking middle of link context menu', async ({
comfyPage comfyPage
}) => { }) => {
const loadCheckpointNode = ( const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint') await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0] )[0]
const clipEncodeNode = ( const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0] )[0]
const slot1 = await loadCheckpointNode.getOutput(1) const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0) const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint( const middlePoint = getMiddlePoint(
await slot1.getPosition(), await slot1.getPosition(),
await slot2.getPosition() await slot2.getPosition()
) )
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' }) .locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
.click() .click()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_context_menu.png' 'native_reroute_context_menu.png'
) )
}) })
test('Can delete link that is connected to two reroutes', async ({ test('Can delete link that is connected to two reroutes', async ({
comfyPage comfyPage
}) => { }) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695 // https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow( await comfyPage.loadWorkflow(
'reroute/single-native-reroute-default-workflow' 'reroute/single-native-reroute-default-workflow'
) )
// To find the clickable midpoint button, we use the hardcoded value from the browser logs // To find the clickable midpoint button, we use the hardcoded value from the browser logs
// since the link is a bezier curve and not a straight line. // since the link is a bezier curve and not a straight line.
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 } const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
// Click the middle point of the link to open the context menu. // Click the middle point of the link to open the context menu.
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
// Click the "Delete" context menu option. // Click the "Delete" context menu option.
await comfyPage.page await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' }) .locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
.click() .click()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_delete_from_midpoint_context_menu.png' 'native_reroute_delete_from_midpoint_context_menu.png'
) )
}) })
}) }
)

View File

@@ -7,43 +7,49 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Canvas Right Click Menu', () => { test.describe(
test('Can add node', async ({ comfyPage }) => { 'Canvas Right Click Menu',
await comfyPage.rightClickCanvas() { tag: ['@screenshot', '@ui'] },
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') () => {
await comfyPage.page.getByText('Add Node').click() test('Can add node', async ({ comfyPage }) => {
await comfyPage.nextFrame() await comfyPage.rightClickCanvas()
await comfyPage.page.getByText('loaders').click() await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.nextFrame() await comfyPage.page.getByText('Add Node').click()
await comfyPage.page.getByText('Load VAE').click() await comfyPage.nextFrame()
await comfyPage.nextFrame() await comfyPage.page.getByText('loaders').click()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png') await comfyPage.nextFrame()
}) await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
})
test('Can add group', async ({ comfyPage }) => { test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas() await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click() await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png') await expect(comfyPage.canvas).toHaveScreenshot(
}) 'add-group-group-added.png'
)
})
test('Can convert to group node', async ({ comfyPage }) => { test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes() await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png') await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas() await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)') await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP') await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter') await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' }) await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png' 'right-click-node-group-node.png'
) )
}) })
}) }
)
test.describe('Node Right Click Menu', () => { test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
test('Can open properties panel', async ({ comfyPage }) => { test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode() await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')

View File

@@ -10,7 +10,7 @@ test.beforeEach(async ({ comfyPage }) => {
const BLUE_COLOR = 'rgb(51, 51, 85)' const BLUE_COLOR = 'rgb(51, 51, 85)'
const RED_COLOR = 'rgb(85, 51, 51)' const RED_COLOR = 'rgb(85, 51, 51)'
test.describe('Selection Toolbox', () => { test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
}) })

View File

@@ -7,178 +7,190 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Selection Toolbox - More Options Submenus', () => { test.describe(
test.beforeEach(async ({ comfyPage }) => { 'Selection Toolbox - More Options Submenus',
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) { tag: '@ui' },
await comfyPage.loadWorkflow('nodes/single_ksampler') () => {
await comfyPage.nextFrame() test.beforeEach(async ({ comfyPage }) => {
await comfyPage.selectNodes(['KSampler']) await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.nextFrame() await comfyPage.loadWorkflow('nodes/single_ksampler')
})
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await comfyPage.page.click('[data-testid="more-options-button"]')
await comfyPage.nextFrame()
const menuOptionsVisible = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisible) {
return
}
await moreOptionsBtn.click({ force: true })
await comfyPage.nextFrame()
const menuOptionsVisibleAfterClick = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisibleAfterClick) {
return
}
throw new Error('Could not open More Options menu - popover not showing')
}
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
await openMoreOptions(comfyPage)
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
})
await expect(nodeInfoButton).toBeVisible()
await nodeInfoButton.click()
await comfyPage.nextFrame()
})
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
timeout: 5000
})
await comfyPage.page.getByText('Box', { exact: true }).click()
await comfyPage.nextFrame()
const newShape = await nodeRef.getProperty<number>('shape')
expect(newShape).not.toBe(initialShape)
expect(newShape).toBe(1)
})
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialColor = await nodeRef.getProperty<string | undefined>('color')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
await blueSwatch.first().click()
await comfyPage.nextFrame()
const newColor = await nodeRef.getProperty<string | undefined>('color')
expect(newColor).toBe('#223')
if (initialColor) {
expect(newColor).not.toBe(initialColor)
}
})
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await openMoreOptions(comfyPage)
await comfyPage.page
.getByText('Rename', { exact: true })
.click({ force: true })
const input = comfyPage.page.locator(
'.group-title-editor.node-title-editor .editable-text input'
)
await expect(input).toBeVisible()
await input.fill('RenamedNode')
await input.press('Enter')
await comfyPage.nextFrame()
const newTitle = await nodeRef.getProperty<string>('title')
expect(newTitle).toBe('RenamedNode')
})
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nextFrame()
})
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await comfyPage.page.click('[data-testid="more-options-button"]')
await comfyPage.nextFrame()
const menuOptionsVisible = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisible) {
return
}
await moreOptionsBtn.click({ force: true })
await comfyPage.nextFrame()
const menuOptionsVisibleAfterClick = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisibleAfterClick) {
return
}
throw new Error('Could not open More Options menu - popover not showing')
} }
await comfyPage.page test('opens Node Info from More Options menu', async ({ comfyPage }) => {
.locator('#graph-canvas') await openMoreOptions(comfyPage)
.click({ position: { x: 0, y: 50 }, force: true }) const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
})
await expect(nodeInfoButton).toBeVisible()
await nodeInfoButton.click()
await comfyPage.nextFrame()
})
await comfyPage.nextFrame() test('changes node shape via Shape submenu', async ({ comfyPage }) => {
await expect( const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
comfyPage.page.getByText('Rename', { exact: true }) const initialShape = await nodeRef.getProperty<number>('shape')
).not.toBeVisible()
})
test('closes More Options menu when clicking the button again (toggle)', async ({ await openMoreOptions(comfyPage)
comfyPage await comfyPage.page.getByText('Shape', { exact: true }).hover()
}) => { await expect(
await openMoreOptions(comfyPage) comfyPage.page.getByText('Box', { exact: true })
await expect( ).toBeVisible({
comfyPage.page.getByText('Rename', { exact: true }) timeout: 5000
).toBeVisible({ timeout: 5000 }) })
await comfyPage.page.getByText('Box', { exact: true }).click()
await comfyPage.nextFrame()
await comfyPage.page.evaluate(() => { const newShape = await nodeRef.getProperty<number>('shape')
const btn = document.querySelector('[data-testid="more-options-button"]') expect(newShape).not.toBe(initialShape)
if (btn) { expect(newShape).toBe(1)
const event = new MouseEvent('click', { })
bubbles: true,
cancelable: true, test('changes node color via Color submenu swatch', async ({
view: window, comfyPage
detail: 1 }) => {
}) const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
btn.dispatchEvent(event) const initialColor = await nodeRef.getProperty<string | undefined>(
'color'
)
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
await blueSwatch.first().click()
await comfyPage.nextFrame()
const newColor = await nodeRef.getProperty<string | undefined>('color')
expect(newColor).toBe('#223')
if (initialColor) {
expect(newColor).not.toBe(initialColor)
} }
}) })
await comfyPage.nextFrame()
await expect( test('renames a node using Rename action', async ({ comfyPage }) => {
comfyPage.page.getByText('Rename', { exact: true }) const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
).not.toBeVisible() await openMoreOptions(comfyPage)
}) await comfyPage.page
}) .getByText('Rename', { exact: true })
.click({ force: true })
const input = comfyPage.page.locator(
'.group-title-editor.node-title-editor .editable-text input'
)
await expect(input).toBeVisible()
await input.fill('RenamedNode')
await input.press('Enter')
await comfyPage.nextFrame()
const newTitle = await nodeRef.getProperty<string>('title')
expect(newTitle).toBe('RenamedNode')
})
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame()
}
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
test('closes More Options menu when clicking the button again (toggle)', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
await comfyPage.page.evaluate(() => {
const btn = document.querySelector(
'[data-testid="more-options-button"]'
)
if (btn) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
detail: 1
})
btn.dispatchEvent(event)
}
})
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
}
)

View File

@@ -12,7 +12,7 @@ const SELECTORS = {
promptDialog: '.graphdialog input' promptDialog: '.graphdialog input'
} as const } as const
test.describe('Subgraph Slot Rename Dialog', () => { test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })

View File

@@ -16,7 +16,7 @@ const SELECTORS = {
domWidget: '.comfy-multiline-input' domWidget: '.comfy-multiline-input'
} as const } as const
test.describe('Subgraph Operations', () => { test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })

View File

@@ -13,7 +13,7 @@ async function checkTemplateFileExists(
return response.ok() return response.ok()
} }
test.describe('Templates', () => { test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false) await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
@@ -207,109 +207,114 @@ test.describe('Templates', () => {
await expect(nav).toBeVisible() // Nav should be visible at tablet size await expect(nav).toBeVisible() // Nav should be visible at tablet size
}) })
test('template cards descriptions adjust height dynamically', async ({ test(
comfyPage 'template cards descriptions adjust height dynamically',
}) => { { tag: '@screenshot' },
// Setup test by intercepting templates response to inject cards with varying description lengths async ({ comfyPage }) => {
await comfyPage.page.route('**/templates/index.json', async (route, _) => { // Setup test by intercepting templates response to inject cards with varying description lengths
const response = [ await comfyPage.page.route(
{ '**/templates/index.json',
moduleName: 'default', async (route, _) => {
title: 'Test Templates', const response = [
type: 'image',
templates: [
{ {
name: 'short-description', moduleName: 'default',
title: 'Short Description', title: 'Test Templates',
mediaType: 'image', type: 'image',
mediaSubtype: 'webp', templates: [
description: 'This is a short description.' {
}, name: 'short-description',
{ title: 'Short Description',
name: 'medium-description', mediaType: 'image',
title: 'Medium Description', mediaSubtype: 'webp',
mediaType: 'image', description: 'This is a short description.'
mediaSubtype: 'webp', },
description: {
'This is a medium length description that should take up two lines on most displays.' name: 'medium-description',
}, title: 'Medium Description',
{ mediaType: 'image',
name: 'long-description', mediaSubtype: 'webp',
title: 'Long Description', description:
mediaType: 'image', 'This is a medium length description that should take up two lines on most displays.'
mediaSubtype: 'webp', },
description: {
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.' name: 'long-description',
title: 'Long Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
}
]
} }
] ]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
} }
] )
await route.fulfill({
status: 200, // Mock the thumbnail images to avoid 404s
body: JSON.stringify(response), await comfyPage.page.route('**/templates/**.webp', async (route) => {
headers: { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'image/webp',
'Cache-Control': 'no-store' 'Cache-Control': 'no-store'
} }
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
}) })
})
// Mock the thumbnail images to avoid 404s // Open templates dialog
await comfyPage.page.route('**/templates/**.webp', async (route) => { await comfyPage.executeCommand('Comfy.BrowseTemplates')
const headers = { await expect(comfyPage.templates.content).toBeVisible()
'Content-Type': 'image/webp',
'Cache-Control': 'no-store'
}
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
})
// Open templates dialog // Wait for cards to load
await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(
await expect(comfyPage.templates.content).toBeVisible() comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
).toBeVisible({ timeout: 5000 })
// Wait for cards to load // Verify all three cards with different descriptions are visible
await expect( const shortDescCard = comfyPage.page.locator(
comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]' '[data-testid="template-workflow-short-description"]'
) )
).toBeVisible({ timeout: 5000 }) const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
// Verify all three cards with different descriptions are visible await expect(shortDescCard).toBeVisible()
const shortDescCard = comfyPage.page.locator( await expect(mediumDescCard).toBeVisible()
'[data-testid="template-workflow-short-description"]' await expect(longDescCard).toBeVisible()
)
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
await expect(shortDescCard).toBeVisible() // Verify descriptions are visible and have line-clamp class
await expect(mediumDescCard).toBeVisible() // The description is in a p tag with text-muted class
await expect(longDescCard).toBeVisible() const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
// Verify descriptions are visible and have line-clamp class await expect(shortDesc).toContainText('short description')
// The description is in a p tag with text-muted class await expect(mediumDesc).toContainText('medium length description')
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2') await expect(longDesc).toContainText('much longer description')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
await expect(shortDesc).toContainText('short description') // Verify grid layout maintains consistency
await expect(mediumDesc).toContainText('medium length description') const templateGrid = comfyPage.page.locator(
await expect(longDesc).toContainText('much longer description') '[data-testid="template-workflows-content"]'
)
// Verify grid layout maintains consistency await expect(templateGrid).toBeVisible()
const templateGrid = comfyPage.page.locator( await expect(templateGrid).toHaveScreenshot(
'[data-testid="template-workflows-content"]' 'template-grid-varying-content.png'
) )
await expect(templateGrid).toBeVisible() }
await expect(templateGrid).toHaveScreenshot( )
'template-grid-varying-content.png'
)
})
}) })

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Settings Search functionality', () => { test.describe('Settings Search functionality', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
// Register test settings to verify hidden/deprecated filtering // Register test settings to verify hidden/deprecated filtering
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {

View File

@@ -5,7 +5,7 @@ import { userSelectPageFixture as test } from '../fixtures/UserSelectPage'
/** /**
* Expects ComfyUI backend to be launched with `--multi-user` flag. * Expects ComfyUI backend to be launched with `--multi-user` flag.
*/ */
test.describe('User Select View', () => { test.describe('User Select View', { tag: '@settings' }, () => {
test.beforeEach(async ({ userSelectPage, page }) => { test.beforeEach(async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url) await page.goto(userSelectPage.url)
await page.evaluate(() => { await page.evaluate(() => {

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { SystemStats } from '../../src/schemas/apiSchema' import type { SystemStats } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Version Mismatch Warnings', () => { test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100' const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'
const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0' const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0'

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Viewport', () => { test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => {
test('Fits view to nodes when saved viewport position is offscreen', async ({ test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage comfyPage
}) => { }) => {

View File

@@ -5,7 +5,7 @@ import {
const CREATE_GROUP_HOTKEY = 'Control+g' const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', () => { test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true) await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)

View File

@@ -9,10 +9,14 @@ test.describe('Vue Nodes Canvas Pan', () => {
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
test('@mobile Can pan with touch', async ({ comfyPage }) => { test(
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 }) '@mobile Can pan with touch',
await expect(comfyPage.canvas).toHaveScreenshot( { tag: '@screenshot' },
'vue-nodes-paned-with-touch.png' async ({ comfyPage }) => {
) await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
}) await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
}
)
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -10,25 +10,30 @@ test.describe('Vue Nodes Zoom', () => {
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
test('should not capture drag while zooming with ctrl+shift+drag', async ({ test(
comfyPage 'should not capture drag while zooming with ctrl+shift+drag',
}) => { { tag: '@screenshot' },
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') async ({ comfyPage }) => {
const nodeBoundingBox = await checkpointNode.boundingBox() const checkpointNode =
if (!nodeBoundingBox) throw new Error('Node bounding box not available') comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBoundingBox = await checkpointNode.boundingBox()
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2 const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2 const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over // Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
// the node. The node should not capture the drag while drag-zooming. // the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift') await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop( await comfyPage.dragAndDrop(
{ x: 200, y: 300 }, { x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY } { x: nodeMidpointX, y: nodeMidpointY }
) )
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png') await expect(comfyPage.canvas).toHaveScreenshot(
}) 'zoomed-in-ctrl-shift.png'
)
}
)
}) })

View File

@@ -98,7 +98,7 @@ async function connectSlots(
await nextFrame() await nextFrame()
} }
test.describe('Vue Node Link Interaction', () => { test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -5,7 +5,7 @@ import {
import type { ComfyPage } from '../../../../fixtures/ComfyPage' import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../../../helpers/fitToView' import { fitToViewInstant } from '../../../../helpers/fitToView'
test.describe('Vue Node Bring to Front', () => { test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -1,8 +1,8 @@
import { import {
type ComfyPage,
comfyExpect as expect, comfyExpect as expect,
comfyPageFixture as test comfyPageFixture as test
} from '../../../../fixtures/ComfyPage' } from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import type { Position } from '../../../../fixtures/types' import type { Position } from '../../../../fixtures/types'
test.describe('Vue Node Moving', () => { test.describe('Vue Node Moving', () => {
@@ -29,39 +29,47 @@ test.describe('Vue Node Moving', () => {
expect(diffY).toBeGreaterThan(0) expect(diffY).toBeGreaterThan(0)
} }
test('should allow moving nodes by dragging', async ({ comfyPage }) => { test(
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) 'should allow moving nodes by dragging',
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, { { tag: '@screenshot' },
x: 256, async ({ comfyPage }) => {
y: 256 const loadCheckpointHeaderPos =
}) await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos) await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png') await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
}) }
)
test('@mobile should allow moving nodes by dragging on touch devices', async ({ test(
comfyPage '@mobile should allow moving nodes by dragging on touch devices',
}) => { { tag: '@screenshot' },
// Disable minimap (gets in way of the node on small screens) async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Minimap.Visible', false) // Disable minimap (gets in way of the node on small screens)
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) const loadCheckpointHeaderPos =
await comfyPage.panWithTouch( await getLoadCheckpointHeaderPos(comfyPage)
{ await comfyPage.panWithTouch(
x: 64, {
y: 64 x: 64,
}, y: 64
loadCheckpointHeaderPos },
) loadCheckpointHeaderPos
)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos) await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-moved-node-touch.png' 'vue-node-moved-node-touch.png'
) )
}) }
)
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -15,22 +15,25 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
test('should allow toggling bypass on a selected node with hotkey', async ({ test(
comfyPage 'should allow toggling bypass on a selected node with hotkey',
}) => { { tag: '@screenshot' },
await comfyPage.page.getByText('Load Checkpoint').click() async ({ comfyPage }) => {
await comfyPage.page.keyboard.press(BYPASS_HOTKEY) await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') const checkpointNode =
await expect(checkpointNode).toHaveClass(BYPASS_CLASS) comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await comfyPage.nextFrame() await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await expect(comfyPage.canvas).toHaveScreenshot( await comfyPage.nextFrame()
'vue-node-bypassed-state.png' await expect(comfyPage.canvas).toHaveScreenshot(
) 'vue-node-bypassed-state.png'
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY) await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
}) }
)
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({ test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage comfyPage

View File

@@ -3,7 +3,7 @@ import {
comfyPageFixture as test comfyPageFixture as test
} from '../../../fixtures/ComfyPage' } from '../../../fixtures/ComfyPage'
test.describe('Vue Node Custom Colors', () => { test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)

View File

@@ -12,19 +12,24 @@ test.describe('Vue Node Mute', () => {
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
test('should allow toggling mute on a selected node with hotkey', async ({ test(
comfyPage 'should allow toggling mute on a selected node with hotkey',
}) => { { tag: '@screenshot' },
await comfyPage.page.getByText('Load Checkpoint').click() async ({ comfyPage }) => {
await comfyPage.page.keyboard.press(MUTE_HOTKEY) await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') const checkpointNode =
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY) comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png') await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-muted-state.png'
)
await comfyPage.page.keyboard.press(MUTE_HOTKEY) await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY) await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
}) }
)
test('should allow toggling mute on multiple selected nodes with hotkey', async ({ test('should allow toggling mute on multiple selected nodes with hotkey', async ({
comfyPage comfyPage

View File

@@ -9,13 +9,17 @@ test.describe('Vue Upload Widgets', () => {
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
test('should hide canvas-only upload buttons', async ({ comfyPage }) => { test(
await comfyPage.setup() 'should hide canvas-only upload buttons',
await comfyPage.loadWorkflow('widgets/all_load_widgets') { tag: '@screenshot' },
await comfyPage.vueNodes.waitForNodes() async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png' 'vue-nodes-upload-widgets.png'
) )
}) }
)
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Combo text widget', () => { test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Truncates text when resized', async ({ comfyPage }) => { test('Truncates text when resized', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.2, 1) await comfyPage.resizeLoadCheckpointNode(0.2, 1)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -79,7 +79,7 @@ test.describe('Combo text widget', () => {
}) })
}) })
test.describe('Boolean widget', () => { test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can toggle', async ({ comfyPage }) => { test('Can toggle', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget') await comfyPage.loadWorkflow('widgets/boolean_widget')
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png') await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
@@ -92,7 +92,7 @@ test.describe('Boolean widget', () => {
}) })
}) })
test.describe('Slider widget', () => { test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can drag adjust value', async ({ comfyPage }) => { test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider') await comfyPage.loadWorkflow('inputs/simple_slider')
const node = (await comfyPage.getFirstNodeRef())! const node = (await comfyPage.getFirstNodeRef())!
@@ -113,7 +113,7 @@ test.describe('Slider widget', () => {
}) })
}) })
test.describe('Number widget', () => { test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can drag adjust value', async ({ comfyPage }) => { test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/seed_widget') await comfyPage.loadWorkflow('widgets/seed_widget')
@@ -134,22 +134,28 @@ test.describe('Number widget', () => {
}) })
}) })
test.describe('Dynamic widget manipulation', () => { test.describe(
test('Auto expand node when widget is added dynamically', async ({ 'Dynamic widget manipulation',
comfyPage { tag: ['@screenshot', '@widget'] },
}) => { () => {
await comfyPage.loadWorkflow('nodes/single_ksampler') test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10) window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true) window['graph'].setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'ksampler_widget_added.png'
)
}) })
}
)
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png') test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe('Image widget', () => {
test('Can load image', async ({ comfyPage }) => { test('Can load image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget') await comfyPage.loadWorkflow('widgets/load_image_widget')
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png') await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png')
@@ -236,99 +242,103 @@ test.describe('Image widget', () => {
}) })
}) })
test.describe('Animated image widget', () => { test.describe(
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718 'Animated image widget',
test.skip('Shows preview of uploaded animated image', async ({ { tag: ['@screenshot', '@widget'] },
comfyPage () => {
}) => { // https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
await comfyPage.loadWorkflow('widgets/load_animated_webp') test.skip('Shows preview of uploaded animated image', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node // Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType( const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest' 'DevToolsLoadAnimatedImageTest'
) )
const loadAnimatedWebpNode = nodes[0] const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition() const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node // Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', { await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y } dropPosition: { x, y }
})
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped.png'
)
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the image preview to change to the next frame of the animation
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped_next_frame.png'
)
}) })
// Expect the image preview to change automatically test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot( await comfyPage.loadWorkflow('widgets/load_animated_webp')
'animated_image_preview_drag_and_dropped.png'
)
// Move mouse and click on canvas to trigger render // Get position of the load animated webp node
await comfyPage.page.mouse.click(64, 64) const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Expect the image preview to change to the next frame of the animation // Drag and drop image file onto the load animated webp node
await expect(comfyPage.canvas).toHaveScreenshot( await comfyPage.dragAndDropFile('animated_webp.webp', {
'animated_image_preview_drag_and_dropped_next_frame.png' dropPosition: { x, y },
) waitForUpload: true
}) })
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => { // Expect the filename combo value to be updated
await comfyPage.loadWorkflow('widgets/load_animated_webp') const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
// Get position of the load animated webp node expect(filename).toContain('animated_webp.webp')
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y },
waitForUpload: true
}) })
// Expect the filename combo value to be updated test('Can preview saved animated webp image', async ({ comfyPage }) => {
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0) await comfyPage.loadWorkflow('widgets/save_animated_webp')
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
})
test('Can preview saved animated webp image', async ({ comfyPage }) => { // Get position of the load animated webp node
await comfyPage.loadWorkflow('widgets/save_animated_webp') const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Get position of the load animated webp node // Drag and drop image file onto the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType( await comfyPage.dragAndDropFile('animated_webp.webp', {
'DevToolsLoadAnimatedImageTest' dropPosition: { x, y }
) })
const loadAnimatedWebpNode = loadNodes[0] await comfyPage.nextFrame()
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node // Get the SaveAnimatedWEBP node
await comfyPage.dragAndDropFile('animated_webp.webp', { const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
dropPosition: { x, y } const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
}) })
await comfyPage.nextFrame() }
)
// Get the SaveAnimatedWEBP node test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => {
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
})
})
test.describe('Load audio widget', () => {
test('Can load audio', async ({ comfyPage }) => { test('Can load audio', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_audio_widget') await comfyPage.loadWorkflow('widgets/load_audio_widget')
// Wait for the audio widget to be rendered in the DOM // Wait for the audio widget to be rendered in the DOM
@@ -338,7 +348,7 @@ test.describe('Load audio widget', () => {
}) })
}) })
test.describe('Unserialized widgets', () => { test.describe('Unserialized widgets', { tag: '@widget' }, () => {
test('Unserialized widgets values do not mark graph as modified', async ({ test('Unserialized widgets values do not mark graph as modified', async ({
comfyPage comfyPage
}) => { }) => {

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