Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions
d703bf729a [automated] Update test expectations 2025-10-06 20:31:30 +00:00
bymyself
e197273bc2 trigger test expectation regeneration
This empty commit is created to trigger CI/CD pipelines that will
regenerate test expectations and update snapshots as needed.
2025-10-06 12:50:47 -07:00
759 changed files with 22131 additions and 42042 deletions

View File

@@ -458,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
```bash
sleep 10
gh run list --workflow=release-draft-create.yaml --limit=1
gh run list --workflow=release.yaml --limit=1
```
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
- Create a `core/x.yy` branch for the PREVIOUS minor version
- Apply branch protection rules
- Document the feature freeze policy
```bash
# Monitor branch creation (for minor/major releases)
gh run list --workflow=release-branch-create.yaml --limit=1
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
```
4. If workflow didn't trigger due to [skip ci]:
```bash
@@ -477,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
5. If workflow triggered, monitor execution:
```bash
WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
gh run watch ${WORKFLOW_RUN_ID}
```

View File

@@ -246,7 +246,7 @@ For each commit:
3. Merge the PR: `gh pr merge --merge`
4. Monitor release workflow:
```bash
gh run list --workflow=release-draft-create.yaml --limit=1
gh run list --workflow=release.yaml --limit=1
gh run watch
```
5. Track progress:

View File

@@ -5,10 +5,6 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
# Proxy target of the local development server
# Note: localhost:8188 does not work.
# Cloud auto-detection: Setting this to any *.comfy.org URL automatically enables
# cloud mode (DISTRIBUTION=cloud) without needing to set DISTRIBUTION separately.
# Examples: https://testcloud.comfy.org/, https://stagingcloud.comfy.org/,
# https://pr-123.testenvs.comfy.org/, https://cloud.comfy.org/
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# Allow dev server access from remote IP addresses.

1
.gitattributes vendored
View File

@@ -7,7 +7,6 @@
*.json text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.snap text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.yaml text eol=lf

View File

@@ -1,55 +0,0 @@
name: Setup ComfyUI Server
description: 'Setup ComfyUI server for continuous integration (with ComfyUI_devtools node installed)'
inputs:
extra_server_params:
description: 'Additional parameters to pass to ComfyUI server'
required: false
default: ''
launch_server:
description: 'Whether to launch the server after setup'
required: false
default: 'false'
runs:
using: 'composite'
steps:
# Note: this workflow assume frontend repo is checked out and is built in ../dist
# Checkout ComfyUI repo, install the dev_tools node and start server
- name: Checkout ComfyUI
uses: actions/checkout@v5
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
- name: Install ComfyUI_devtools from frontend repo
shell: bash
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
if ! cp -r ./tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/; then
echo "::error::Failed to copy ComfyUI_devtools from ./tools/devtools/"
echo "::error::This action assumes the ComfyUI_frontend repository is checked out in the current working directory."
echo "::error::Please ensure you have run 'actions/checkout@v5' before calling this action."
exit 1
fi
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
shell: bash
working-directory: ComfyUI
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
- name: Start ComfyUI server
if: ${{ inputs.launch_server == 'true' }}
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../dist ${{ inputs.extra_server_params }} &
wait-for-it --service 127.0.0.1:8188 -t 600

View File

@@ -1,16 +1,31 @@
name: Setup ComfyUI Frontend
description: 'Install nodejs/pnpm/dependencies and optionally build ComfyUI_frontend'
name: Setup Frontend
description: 'Setup ComfyUI frontend development environment'
inputs:
include_build_step:
description: 'Include the build step to build the frontend. Set to true for workflows that need a built frontend'
extra_server_params:
description: 'Additional parameters to pass to ComfyUI server'
required: false
default: 'false'
default: ''
runs:
using: 'composite'
steps:
# Note: this workflow assume frontend repo is checked out in the root of the workspace
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Copy ComfyUI_devtools from frontend repo
shell: bash
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
# Install pnpm, Node.js, build frontend
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -21,25 +36,32 @@ runs:
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
# Restore tool caches before running any build/lint operations
- name: Restore tool output cache
uses: actions/cache/restore@v4
- name: Setup Python
uses: actions/setup-python@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 }}-
python-version: '3.10'
- name: Install dependencies
- name: Install Python requirements
shell: bash
run: pnpm install --frozen-lockfile
working-directory: ComfyUI
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
- name: Build ComfyUI_frontend
if: ${{ inputs.include_build_step == 'true' }}
- name: Build & Install ComfyUI_frontend
shell: bash
run: pnpm build
working-directory: ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Start ComfyUI server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist ${{ inputs.extra_server_params }} &
wait-for-it --service 127.0.0.1:8188 -t 600

View File

@@ -6,6 +6,7 @@ runs:
- name: Detect Playwright version
id: detect-version
shell: bash
working-directory: ComfyUI_frontend
run: |
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
@@ -21,8 +22,10 @@ runs:
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
shell: bash
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers (operating system dependencies)
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
shell: bash
run: pnpm exec playwright install-deps
working-directory: ComfyUI_frontend

View File

@@ -1,21 +0,0 @@
# GitHub Workflows
## Naming Convention
Workflow files follow a consistent naming pattern: `<prefix>-<descriptive-name>.yaml`
### Category Prefixes
| Prefix | Purpose | Example |
| ---------- | ----------------------------------- | ------------------------------------ |
| `ci-` | Testing, linting, validation | `ci-tests-e2e.yaml` |
| `release-` | Version management, publishing | `release-version-bump.yaml` |
| `pr-` | PR automation (triggered by labels) | `pr-claude-review.yaml` |
| `api-` | External Api type generation | `api-update-registry-api-types.yaml` |
| `i18n-` | Internationalization updates | `i18n-update-core.yaml` |
## Documentation
Each workflow file contains comments explaining its purpose, triggers, and behavior. For specific details about what each workflow does, refer to the comments at the top of each `.yaml` file.
For GitHub Actions documentation, see [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).

View File

@@ -1,4 +1,4 @@
name: PR Backport
name: Auto Backport
on:
pull_request_target:
@@ -69,139 +69,70 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Collect backport targets
id: targets
- name: Check if backports already exist
id: check-existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
TARGETS=()
declare -A SEEN=()
# Check for existing backport PRs for this PR number
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
if [ -z "$EXISTING_BACKPORTS" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
# For manual triggers with force_rerun, proceed anyway
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "::warning::Force rerun requested - existing backports will be updated"
exit 0
fi
echo "Found existing backport PRs:"
echo "$EXISTING_BACKPORTS"
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
- name: Extract version labels
if: steps.check-existing.outputs.skip != 'true'
id: versions
run: |
# Extract version labels (e.g., "1.24", "1.22")
VERSIONS=""
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# For manual triggers, get labels from the PR
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
# For automatic triggers, extract from PR event
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi
add_target() {
local label="$1"
local target="$2"
if [ -z "$target" ]; then
return
fi
target=$(echo "$target" | xargs)
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
return
fi
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
TARGETS+=("$target")
SEEN["$target"]=1
else
echo "::warning::Label '${label}' references missing branch '${target}'"
fi
}
while IFS= read -r label; do
[ -z "$label" ] && continue
if [[ "$label" =~ ^branch:(.+)$ ]]; then
add_target "$label" "${BASH_REMATCH[1]}"
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
add_target "$label" "${BASH_REMATCH[1]}"
elif [[ "$label" =~ ^core\/([0-9]+)\.([0-9]+)$ ]]; then
SAFE_MAJOR="${BASH_REMATCH[1]}"
SAFE_MINOR="${BASH_REMATCH[2]}"
add_target "$label" "core/${SAFE_MAJOR}.${SAFE_MINOR}"
elif [[ "$label" =~ ^cloud\/([0-9]+)\.([0-9]+)$ ]]; then
SAFE_MAJOR="${BASH_REMATCH[1]}"
SAFE_MINOR="${BASH_REMATCH[2]}"
add_target "$label" "cloud/${SAFE_MAJOR}.${SAFE_MINOR}"
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
add_target "$label" "core/${label}"
fi
done <<< "$LABELS"
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
exit 1
fi
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
echo "Found backport targets: ${TARGETS[*]}"
- name: Filter already backported targets
id: filter-targets
env:
EVENT_NAME: ${{ github.event_name }}
FORCE_RERUN_INPUT: >-
${{ github.event_name == 'workflow_dispatch' && inputs.force_rerun
|| 'false' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: >-
${{ github.event_name == 'workflow_dispatch' && inputs.pr_number
|| github.event.pull_request.number }}
run: |
set -euo pipefail
REQUESTED_TARGETS="${{ steps.targets.outputs.targets }}"
if [ -z "$REQUESTED_TARGETS" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "pending-targets=" >> $GITHUB_OUTPUT
exit 0
fi
FORCE_RERUN=false
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$FORCE_RERUN_INPUT" = "true" ]; then
FORCE_RERUN=true
fi
mapfile -t EXISTING_BRANCHES < <(
git ls-remote --heads origin "backport-${PR_NUMBER}-to-*" || true
)
PENDING=()
SKIPPED=()
for target in $REQUESTED_TARGETS; do
SAFE_TARGET=$(echo "$target" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
if [ "$FORCE_RERUN" = true ]; then
PENDING+=("$target")
continue
fi
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
SKIPPED+=("$target")
else
PENDING+=("$target")
for label in $LABELS; do
# Match version labels like "1.24" (major.minor only)
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Validate the branch exists before adding to list
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
VERSIONS="${VERSIONS}${label} "
else
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
fi
fi
done
SKIPPED_JOINED="${SKIPPED[*]:-}"
PENDING_JOINED="${PENDING[*]:-}"
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
if [ -z "$PENDING_JOINED" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
if [ -n "$SKIPPED_JOINED" ]; then
echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}"
fi
else
echo "skip=false" >> $GITHUB_OUTPUT
if [ -n "$SKIPPED_JOINED" ]; then
echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}"
fi
if [ -z "$VERSIONS" ]; then
echo "::error::No version labels found (e.g., 1.24, 1.22)"
exit 1
fi
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
echo "Found version labels: ${VERSIONS}"
- name: Backport commits
if: steps.filter-targets.outputs.skip != 'true'
if: steps.check-existing.outputs.skip != 'true'
id: backport
env:
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
@@ -219,25 +150,16 @@ jobs:
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
TARGET_BRANCH="${target}"
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}"
echo "::group::Backporting to ${TARGET_BRANCH}"
TARGET_BRANCH="core/${version}"
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
# Fetch target branch (fail if doesn't exist)
if ! git fetch origin "${TARGET_BRANCH}"; then
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
echo "::endgroup::"
continue
fi
# Check if commit already exists on target branch
if git branch -r --contains "${MERGE_COMMIT}" | grep -q "origin/${TARGET_BRANCH}"; then
echo "::notice::Commit ${MERGE_COMMIT} already exists on ${TARGET_BRANCH}, skipping backport"
FAILED="${FAILED}${TARGET_BRANCH}:already-exists "
FAILED="${FAILED}${version}:branch-missing "
echo "::endgroup::"
continue
fi
@@ -248,7 +170,7 @@ jobs:
# Try cherry-pick
if git cherry-pick "${MERGE_COMMIT}"; then
git push origin "${BACKPORT_BRANCH}"
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
# Return to main (keep the branch, we need it for PR)
git checkout main
@@ -258,7 +180,7 @@ jobs:
git cherry-pick --abort
echo "::error::Cherry-pick failed due to conflicts"
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
# Clean up the failed branch
git checkout main
@@ -276,7 +198,7 @@ jobs:
fi
- name: Create PR for each successful backport
if: steps.filter-targets.outputs.skip != 'true' && steps.backport.outputs.success
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
@@ -292,13 +214,13 @@ jobs:
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r target branch <<< "${backport}"
IFS=':' read -r version branch <<< "${backport}"
if PR_URL=$(gh pr create \
--base "${target}" \
--base "core/${version}" \
--head "${branch}" \
--title "[backport ${target}] ${PR_TITLE}" \
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
--title "[backport ${version}] ${PR_TITLE}" \
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
--label "backport" 2>&1); then
# Extract PR number from URL
@@ -308,14 +230,14 @@ jobs:
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
fi
else
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
# Still try to comment on the original PR about the failure
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
fi
done
- name: Comment on failures
if: steps.filter-targets.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -331,25 +253,16 @@ jobs:
fi
for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r target reason conflicts <<< "${failure}"
IFS=':' read -r version reason conflicts <<< "${failure}"
if [ "${reason}" = "branch-missing" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
elif [ "${reason}" = "already-exists" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
elif [ "${reason}" = "conflicts" ]; then
# Convert comma-separated conflicts back to newlines for display
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
fi
done
- name: Remove needs-backport label
if: steps.filter-targets.outputs.skip != 'true' && success()
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,52 +0,0 @@
name: "CI: Size Data"
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
collect:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
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
run: pnpm build
- name: Collect size data
run: node scripts/size-collect.js
- name: Save PR number & base branch
if: ${{ github.event_name == 'pull_request' }}
run: |
echo ${{ github.event.number }} > ./temp/size/number.txt
echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload size data
uses: actions/upload-artifact@v4
with:
name: size-data
path: temp/size

View File

@@ -1,5 +1,4 @@
name: "PR: Claude Review"
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
name: Claude PR Review
permissions:
contents: read
@@ -12,10 +11,6 @@ on:
pull_request:
types: [labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
wait-for-ci:
runs-on: ubuntu-latest
@@ -28,7 +23,6 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
check-regexp: '^(lint-and-format|test|playwright-tests)'
allowed-conclusions: success,skipped,failure,cancelled,neutral,action_required,timed_out,stale
wait-interval: 30
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -79,10 +73,10 @@ jobs:
with:
label_trigger: "claude-review"
prompt: |
Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly.
CRITICAL: You must post individual inline comments using the gh api commands shown in the file.
DO NOT create a summary comment.
Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly.
CRITICAL: You must post individual inline comments using the gh api commands shown in the file.
DO NOT create a summary comment.
Each issue must be posted as a separate inline comment on the specific line of code.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--max-turns 256 --allowedTools 'Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch'"
@@ -92,9 +86,3 @@ jobs:
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
REPOSITORY: ${{ github.repository }}
- name: Remove claude-review label
if: always()
run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "claude-review"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,4 +1,4 @@
name: Release PyPI Dev
name: Create Dev PyPI Package
on:
workflow_dispatch:
@@ -25,6 +25,17 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
dist
tsconfig.tsbuildinfo
key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
dev-release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -33,7 +44,6 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile

View File

@@ -1,4 +1,4 @@
name: Release Branch Create
name: Create Release Branch
on:
pull_request:
@@ -69,9 +69,6 @@ jobs:
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "prev_minor=$PREV_MINOR" >> $GITHUB_OUTPUT
BASE_COMMIT=$(git rev-parse HEAD)
echo "base_commit=$BASE_COMMIT" >> $GITHUB_OUTPUT
# Get previous major version for comparison
PREV_MAJOR=$(echo $PREV_VERSION | cut -d. -f1)
@@ -90,13 +87,13 @@ jobs:
elif [[ "$MAJOR" -gt "$PREV_MAJOR" && "$MINOR" == "0" && "$PATCH" == "0" ]]; then
# Major version bump (e.g., 1.99.x → 2.0.0)
echo "is_minor_bump=true" >> $GITHUB_OUTPUT
BRANCH_BASE="${PREV_MAJOR}.${PREV_MINOR}"
echo "branch_base=$BRANCH_BASE" >> $GITHUB_OUTPUT
BRANCH_NAME="core/${PREV_MAJOR}.${PREV_MINOR}"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
elif [[ "$MAJOR" == "$PREV_MAJOR" && "$MINOR" -gt "$PREV_MINOR" && "$PATCH" == "0" ]]; then
# Minor version bump (e.g., 1.23.x → 1.24.0)
echo "is_minor_bump=true" >> $GITHUB_OUTPUT
BRANCH_BASE="${MAJOR}.${PREV_MINOR}"
echo "branch_base=$BRANCH_BASE" >> $GITHUB_OUTPUT
BRANCH_NAME="core/${MAJOR}.${PREV_MINOR}"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
else
echo "is_minor_bump=false" >> $GITHUB_OUTPUT
fi
@@ -104,97 +101,64 @@ jobs:
# Return to main branch
git checkout main
- name: Create release branches
id: create_branches
- name: Create release branch
if: steps.check_version.outputs.is_minor_bump == 'true'
run: |
BRANCH_BASE="${{ steps.check_version.outputs.branch_base }}"
PREV_VERSION="${{ steps.check_version.outputs.prev_version }}"
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
if [[ -z "$BRANCH_BASE" ]]; then
echo "::error::Branch base not set; unable to determine release branches"
exit 1
# Check if branch already exists
if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
echo "⚠️ Branch $BRANCH_NAME already exists, skipping creation"
echo "branch_exists=true" >> $GITHUB_ENV
exit 0
else
echo "branch_exists=false" >> $GITHUB_ENV
fi
BASE_COMMIT="${{ steps.check_version.outputs.base_commit }}"
# Create branch from the commit BEFORE the version bump
# This ensures the release branch has the previous minor version
git checkout -b "$BRANCH_NAME" HEAD^1
if [[ -z "$BASE_COMMIT" ]]; then
echo "::error::Base commit not provided; cannot create release branches"
exit 1
fi
# Push the new branch
git push origin "$BRANCH_NAME"
RESULTS_FILE=$(mktemp)
trap 'rm -f "$RESULTS_FILE"' EXIT
echo "✅ Created release branch: $BRANCH_NAME"
echo "This branch is now in feature freeze and will only receive:"
echo "- Bug fixes"
echo "- Critical security patches"
echo "- Documentation updates"
for PREFIX in core cloud; do
BRANCH_NAME="${PREFIX}/${BRANCH_BASE}"
if git ls-remote --exit-code --heads origin \
"$BRANCH_NAME" >/dev/null 2>&1; then
echo "⚠️ Branch $BRANCH_NAME already exists"
echo " Skipping creation for $BRANCH_NAME"
STATUS="exists"
else
# Create branch from the commit BEFORE the version bump
if ! git push origin "$BASE_COMMIT:refs/heads/$BRANCH_NAME"; then
echo "::error::Failed to push release branch $BRANCH_NAME"
exit 1
fi
echo "✅ Created release branch: $BRANCH_NAME"
STATUS="created"
fi
echo "$BRANCH_NAME|$STATUS|$PREV_VERSION" >> "$RESULTS_FILE"
done
{
echo "results<<'EOF'"
cat "$RESULTS_FILE"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Post summary
if: steps.check_version.outputs.is_minor_bump == 'true'
run: |
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
PREV_VERSION="${{ steps.check_version.outputs.prev_version }}"
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
RESULTS="${{ steps.create_branches.outputs.results }}"
if [[ -z "$RESULTS" ]]; then
if [[ "${{ env.branch_exists }}" == "true" ]]; then
cat >> $GITHUB_STEP_SUMMARY << EOF
## 🌿 Release Branch Summary
## 🌿 Release Branch Already Exists
Release branch creation skipped; no eligible branches were found.
The release branch for the previous minor version already exists:
EOF
else
cat >> $GITHUB_STEP_SUMMARY << EOF
## 🌿 Release Branch Created
A new release branch has been created for the previous minor version:
EOF
exit 0
fi
cat >> $GITHUB_STEP_SUMMARY << EOF
## 🌿 Release Branch Summary
- **Branch**: \`$BRANCH_NAME\`
- **Version**: \`$PREV_VERSION\` (feature frozen)
- **Main branch**: \`$CURRENT_VERSION\` (active development)
### Branch Status
EOF
while IFS='|' read -r BRANCH STATUS PREV_VERSION; do
if [[ "$STATUS" == "created" ]]; then
cat >> $GITHUB_STEP_SUMMARY << EOF
- \`$BRANCH\` created from version \`$PREV_VERSION\`
EOF
else
cat >> $GITHUB_STEP_SUMMARY << EOF
- \`$BRANCH\` already existed (based on version \`$PREV_VERSION\`)
EOF
fi
done <<< "$RESULTS"
cat >> $GITHUB_STEP_SUMMARY << EOF
### Branch Policy
Release branches are feature-frozen and only accept:
The \`$BRANCH_NAME\` branch is now in **feature freeze** and will only accept:
- 🐛 Bug fixes
- 🔒 Security patches
- 📚 Documentation updates
@@ -203,9 +167,9 @@ jobs:
### Backporting Changes
To backport a fix:
To backport a fix to this release branch:
1. Create your fix on \`main\` first
2. Cherry-pick to the target release branch
3. Create a PR targeting that branch
4. Apply the matching \`core/x.y\` or \`cloud/x.y\` label
2. Cherry-pick to \`$BRANCH_NAME\`
3. Create a PR targeting \`$BRANCH_NAME\`
4. Use the \`Release\` label on the PR
EOF

View File

@@ -1,4 +1,4 @@
name: Release Draft Create
name: Create Release Draft
on:
pull_request:
@@ -28,6 +28,16 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
tsconfig.tsbuildinfo
key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -45,7 +55,6 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
@@ -117,7 +126,7 @@ jobs:
publish_types:
needs: build
uses: ./.github/workflows/release-npm-types.yaml
uses: ./.github/workflows/publish-frontend-types.yaml
with:
version: ${{ needs.build.outputs.version }}
ref: ${{ github.event.pull_request.merge_commit_sha }}

View File

@@ -1,5 +1,4 @@
name: "CI: Python Validation"
description: "Validates Python code in tools/devtools directory"
name: Devtools Python Check
on:
pull_request:

View File

@@ -1,14 +1,9 @@
name: "CI: Lint Format"
description: "Linting and code formatting validation for pull requests"
name: Lint and Format
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
@@ -33,15 +28,27 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
.eslintcache
tsconfig.tsbuildinfo
.prettierCache
.knip-cache
key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }}
restore-keys: |
lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
lint-format-cache-${{ runner.os }}-
ci-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint with auto-fix
run: pnpm lint:fix
- name: Run Stylelint with auto-fix
run: pnpm stylelint:fix
- name: Run Prettier with auto-format
run: pnpm format
@@ -66,7 +73,6 @@ jobs:
- name: Final validation
run: |
pnpm lint
pnpm stylelint
pnpm format:check
pnpm knip

View File

@@ -1,9 +1,8 @@
name: "CI: Tests E2E (Deploy for Forks)"
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
name: PR Playwright Deploy (Forks)
on:
workflow_run:
workflows: ["CI: Tests E2E"]
workflows: ["Tests CI"]
types: [requested, completed]
env:

View File

@@ -1,104 +0,0 @@
name: "PR: Size Report"
on:
workflow_run:
workflows: ['CI: Size Data']
types:
- completed
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to report on'
required: true
type: number
run_id:
description: 'Size data workflow run ID'
required: true
type: string
permissions:
contents: read
pull-requests: write
issues: write
jobs:
comment:
runs-on: ubuntu-latest
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(
(github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success') ||
github.event_name == 'workflow_dispatch'
)
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
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
uses: dawidd6/action-download-artifact@v11
with:
name: size-data
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
path: temp/size
- name: Set PR number
id: pr-number
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
fi
- name: Set base branch
id: pr-base
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "content=main" >> $GITHUB_OUTPUT
else
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
fi
- name: Download previous size data
uses: dawidd6/action-download-artifact@v11
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: ci-size-data.yaml
event: push
name: size-data
path: temp/size-prev
if_no_artifact_found: warn
- name: Generate size report
run: node scripts/size-report.js > size-report.md
- name: Read size report
id: size-report
uses: juliangruber/read-file-action@v1
with:
path: ./size-report.md
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}
body: |
${{ steps.size-report.outputs.content }}
<!-- COMFYUI_FRONTEND_SIZE -->
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'

View File

@@ -1,9 +1,8 @@
name: "CI: Tests Storybook (Deploy for Forks)"
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
name: PR Storybook Deploy (Forks)
on:
workflow_run:
workflows: ["CI: Tests Storybook"]
workflows: ['Storybook and Chromatic CI']
types: [requested, completed]
env:

View File

@@ -1,309 +0,0 @@
# Setting test expectation screenshots for Playwright
name: "PR: Update Playwright Expectations"
on:
pull_request:
types: [labeled]
issue_comment:
types: [created]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
setup:
runs-on: ubuntu-latest
if: >
( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) ||
( github.event.issue.pull_request &&
github.event_name == 'issue_comment' &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
) &&
startsWith(github.event.comment.body, '/update-playwright') )
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
pr-number: ${{ steps.pr-info.outputs.pr-number }}
branch: ${{ steps.pr-info.outputs.branch }}
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
steps:
- name: Get PR info
id: pr-info
run: |
echo "pr-number=${{ github.event.number || github.event.issue.number }}" >> $GITHUB_OUTPUT
echo "branch=$(gh pr view ${{ github.event.number || github.event.issue.number }} --repo ${{ github.repository }} --json headRefName --jq '.headRefName')" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find Update Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
id: "find-update-comment"
with:
issue-number: ${{ steps.pr-info.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "Updating Playwright Expectations"
- name: Add Starting Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-info.outputs.pr-number }}
body: |
Updating Playwright Expectations
edit-mode: replace
reactions: eyes
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ steps.pr-info.outputs.branch }}
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
# Save expensive build artifacts (Python env, built frontend, node_modules)
# Source code will be checked out fresh in sharded jobs
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
path: |
ComfyUI
dist
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
# Sharded snapshot updates
update-snapshots-sharded:
needs: setup
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
# Checkout source code fresh (not cached)
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ needs.setup.outputs.branch }}
# Restore expensive build artifacts from setup job
- name: Restore cached artifacts
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
with:
fail-on-cache-miss: true
path: |
ComfyUI
dist
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- name: Setup ComfyUI server (from cache)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Run sharded tests with snapshot updates
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright-tests
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
continue-on-error: true
# Identify and stage only changed snapshot files
- name: Stage changed snapshot files
id: changed-snapshots
run: |
echo "=========================================="
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
echo "=========================================="
# Get list of changed snapshot files
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
if [ -z "$changed_files" ]; then
echo "No snapshot changes in this shard"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "✓ Found changed files:"
echo "$changed_files"
file_count=$(echo "$changed_files" | wc -l)
echo "Count: $file_count"
echo "has-changes=true" >> $GITHUB_OUTPUT
echo ""
# Create staging directory
mkdir -p /tmp/changed_snapshots_shard
# Copy only changed files, preserving directory structure
# Strip 'browser_tests/' prefix to avoid double nesting
echo "Copying changed files to staging directory..."
while IFS= read -r file; do
# Remove 'browser_tests/' prefix
file_without_prefix="${file#browser_tests/}"
# Create parent directories
mkdir -p "/tmp/changed_snapshots_shard/$(dirname "$file_without_prefix")"
# Copy file
cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix"
echo " → $file_without_prefix"
done <<< "$changed_files"
echo ""
echo "Staged files for upload:"
find /tmp/changed_snapshots_shard -type f
# Upload ONLY the changed files from this shard
- name: Upload changed snapshots
uses: actions/upload-artifact@v4
if: steps.changed-snapshots.outputs.has-changes == 'true'
with:
name: snapshots-shard-${{ matrix.shardIndex }}
path: /tmp/changed_snapshots_shard/
retention-days: 1
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-shard-${{ matrix.shardIndex }}
path: ./playwright-report/
retention-days: 30
# Merge snapshots and commit
merge-and-commit:
needs: [setup, update-snapshots-sharded]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ needs.setup.outputs.branch }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts
uses: actions/download-artifact@v4
with:
pattern: snapshots-shard-*
path: ./downloaded-snapshots
merge-multiple: false
- name: List downloaded files
run: |
echo "=========================================="
echo "DOWNLOADED SNAPSHOT FILES"
echo "=========================================="
find ./downloaded-snapshots -type f
echo ""
echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)"
# Merge only the changed files into browser_tests
- name: Merge changed snapshots
run: |
set -euo pipefail
echo "=========================================="
echo "MERGING CHANGED SNAPSHOTS"
echo "=========================================="
# Verify target directory exists
if [ ! -d "browser_tests" ]; then
echo "::error::Target directory 'browser_tests' does not exist"
exit 1
fi
merged_count=0
# For each shard's changed files, copy them directly
for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do
if [ ! -d "$shard_dir" ]; then
continue
fi
shard_name=$(basename "$shard_dir")
file_count=$(find "$shard_dir" -type f | wc -l)
if [ "$file_count" -eq 0 ]; then
echo " $shard_name: no files"
continue
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 "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: $merged_count"
- name: Show changes
run: |
echo "=========================================="
echo "CHANGES SUMMARY"
echo "=========================================="
echo ""
echo "Changed files in browser_tests:"
git diff --name-only browser_tests/ | head -20 || echo "No changes"
echo ""
echo "Total changes:"
git diff --name-only browser_tests/ | wc -l || echo "0"
- name: Commit updated expectations
id: commit
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if git diff --quiet browser_tests/; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "=========================================="
echo "COMMITTING CHANGES"
echo "=========================================="
echo "has-changes=true" >> $GITHUB_OUTPUT
git add browser_tests/
git commit -m "[automated] Update test expectations"
echo "Pushing to ${{ needs.setup.outputs.branch }}..."
git push origin ${{ needs.setup.outputs.branch }}
echo "✓ Commit and push successful"
- name: Add Done Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true'
with:
comment-id: ${{ needs.setup.outputs.comment-id }}
issue-number: ${{ needs.setup.outputs.pr-number }}
reactions: +1
reactions-edit-mode: replace
- name: Remove New Browser Test Expectations label
if: always() && github.event_name == 'pull_request'
run: gh pr edit ${{ needs.setup.outputs.pr-number }} --remove-label "New Browser Test Expectations"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -44,7 +44,6 @@ jobs:
contents: read
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ENABLE_MINIFY: 'true'
steps:
- name: Validate inputs
env:
@@ -161,6 +160,20 @@ jobs:
echo "publish_dir=$PUBLISH_DIR" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
- name: Pack (preview only)
shell: bash
working-directory: ${{ steps.pkg.outputs.publish_dir }}
run: |
set -euo pipefail
npm pack --json | tee pack-result.json
- name: Upload package tarball artifact
uses: actions/upload-artifact@v4
with:
name: desktop-ui-npm-tarball-${{ inputs.version }}
path: ${{ steps.pkg.outputs.publish_dir }}/*.tgz
if-no-files-found: error
- name: Check if version already on npm
id: check_npm
env:

View File

@@ -1,4 +1,4 @@
name: Release NPM Types
name: Publish Frontend Types
on:
workflow_dispatch:

View File

@@ -1,5 +1,6 @@
name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
name: Storybook and Chromatic CI
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
on:
workflow_dispatch: # Allow manual triggering
@@ -50,6 +51,19 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -102,6 +116,19 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@@ -1,5 +1,4 @@
name: "CI: Tests E2E"
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
name: Tests CI
on:
push:
@@ -8,37 +7,70 @@ on:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Checkout repository
- name: Checkout ComfyUI
uses: actions/checkout@v5
# Setup Test Environment, build frontend but do not start server yet
- name: Setup ComfyUI server
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Copy ComfyUI_devtools from frontend repo
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
ComfyUI_frontend/.cache
ComfyUI_frontend/tsconfig.tsbuildinfo
key: playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ hashFiles('ComfyUI_frontend/src/**/*.{ts,vue,js}', 'ComfyUI_frontend/*.config.*') }}
restore-keys: |
playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
playwright-setup-cache-${{ runner.os }}-
playwright-tools-cache-${{ runner.os }}-
- name: Build ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
working-directory: ComfyUI_frontend
# Save the entire workspace as cache for later test jobs to restore
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
path: .
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
# Sharded chromium tests
@@ -53,35 +85,54 @@ jobs:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
# download built frontend repo from setup job
- name: Wait for cache propagation
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
with:
fail-on-cache-miss: true
path: .
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
# Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
- name: Setup ComfyUI server
uses: ./.github/actions/setup-comfyui-server
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
launch_server: true
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
version: 10
- uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Setup Playwright
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
# Run sharded tests and upload sharded reports
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
working-directory: ComfyUI_frontend
env:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report
- name: Upload blob report
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: blob-report-chromium-${{ matrix.shardIndex }}
@@ -100,27 +151,45 @@ jobs:
matrix:
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps:
# download built frontend repo from setup job
- name: Wait for cache propagation
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
with:
fail-on-cache-miss: true
path: .
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
# Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
- name: Setup ComfyUI server
uses: ./.github/actions/setup-comfyui-server
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
launch_server: true
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
version: 10
- uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Setup Playwright
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
# Run tests and upload reports
- name: Run Playwright tests (${{ matrix.browser }})
id: playwright
run: |
@@ -130,13 +199,13 @@ jobs:
--reporter=list \
--reporter=html \
--reporter=json
working-directory: ComfyUI_frontend
- name: Upload Playwright report
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.browser }}
path: ./playwright-report/
path: ComfyUI_frontend/playwright-report/
retention-days: 30
# Merge sharded test reports
@@ -145,19 +214,32 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
steps:
- name: Checkout repository
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
# Setup Test Environment, we only need playwright to merge reports
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
working-directory: ComfyUI_frontend
- name: Download blob reports
uses: actions/download-artifact@v4
with:
path: ./all-blob-reports
path: ComfyUI_frontend/all-blob-reports
pattern: blob-report-chromium-*
merge-multiple: true
@@ -168,12 +250,13 @@ jobs:
# Generate JSON report separately with explicit output path
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
working-directory: ComfyUI_frontend
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-report-chromium
path: ./playwright-report/
path: ComfyUI_frontend/playwright-report/
retention-days: 30
#### BEGIN Deployment and commenting (non-forked PRs only)
@@ -189,11 +272,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Get start time
id: start-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -204,7 +287,7 @@ jobs:
"${{ github.head_ref }}" \
"starting" \
"${{ steps.start-time.outputs.time }}"
# Deploy and comment for non-forked PRs only
deploy-and-comment:
needs: [playwright-tests, merge-reports]
@@ -216,21 +299,24 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download all playwright reports
uses: actions/download-artifact@v4
with:
pattern: playwright-report-*
path: reports
- name: Make deployment script executable
run: chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
- name: Deploy reports and comment on PR
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
run: |
bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"completed"
#### END Deployment and commenting (non-forked PRs only)
#### END Deployment and commenting (non-forked PRs only)

View File

@@ -1,5 +1,4 @@
name: 'Api: Update Registry API Types'
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
name: Update Comfy Registry API Types
on:
# Manual trigger
@@ -30,9 +29,26 @@ jobs:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-registry-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache comfy-api repository
uses: actions/cache@v4
with:
path: comfy-api
key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfy-api-repo-${{ runner.os }}-
- name: Checkout comfy-api repository
uses: actions/checkout@v5
with:

View File

@@ -1,5 +1,4 @@
name: 'Api: Update Manager API Types'
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
name: Update ComfyUI-Manager API Types
on:
# Manual trigger
@@ -31,9 +30,26 @@ jobs:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-manager-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache ComfyUI-Manager repository
uses: actions/cache@v4
with:
path: ComfyUI-Manager
key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfyui-manager-repo-${{ runner.os }}-
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
with:

View File

@@ -1,5 +1,4 @@
name: 'Api: Update Electron API Types'
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
name: Update Electron Types
on:
workflow_dispatch:
@@ -26,6 +25,15 @@ jobs:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
electron-types-tools-cache-${{ runner.os }}-
- name: Update electron types
run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest

View File

@@ -1,4 +1,4 @@
name: i18n Update Custom Nodes
name: Update Locales for given custom node repository
on:
workflow_dispatch:
@@ -21,64 +21,90 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- name: Checkout ComfyUI
uses: actions/checkout@v5
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: 'true'
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Install the custom node repository
repository: comfyanonymous/ComfyUI
path: ComfyUI
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
with:
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
- name: Copy ComfyUI_devtools from frontend repo
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Checkout custom node repository
uses: actions/checkout@v5
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install custom node Python requirements
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install ComfyUI requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Install custom node requirements
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
# Start ComfyUI Server
- name: Start ComfyUI Server
shell: bash
working-directory: ComfyUI
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- name: Build & Install ComfyUI_frontend
run: |
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
wait-for-it --service
pnpm install --frozen-lockfile
pnpm build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
working-directory: ComfyUI_frontend
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Setup Playwright
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
working-directory: ComfyUI_frontend
- name: Capture base i18n
run: pnpm exec tsx scripts/diff-i18n capture
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Diff base vs updated i18n
run: pnpm exec tsx scripts/diff-i18n diff
working-directory: ComfyUI_frontend
- name: Update i18n in custom node repository
run: |
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
install -d "$LOCALE_DIR"
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
# Git ops for pushing changes and creating PR
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository

View File

@@ -1,5 +1,4 @@
name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
name: Update Locales
on:
# Manual dispatch for urgent translation updates
@@ -15,35 +14,35 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Frontend
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- name: Cache tool outputs
uses: actions/cache@v4
with:
include_build_step: true
- name: Setup ComfyUI Server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
path: |
ComfyUI_frontend/.cache
ComfyUI_frontend/.cache
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
i18n-tools-cache-${{ runner.os }}-
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
# Update locales, collect new strings and update translations using OpenAI, then commit changes
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
@@ -55,5 +54,6 @@ jobs:
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales"
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git push origin HEAD:${{ github.head_ref }}
working-directory: ComfyUI_frontend

View File

@@ -1,4 +1,4 @@
name: i18n Update Nodes
name: Update Node Definitions Locales
on:
workflow_dispatch:
@@ -13,32 +13,24 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
@@ -52,3 +44,4 @@ jobs:
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies
path: ComfyUI_frontend

View File

@@ -0,0 +1,44 @@
# Setting test expectation screenshots for Playwright
name: Update Playwright Expectations
on:
pull_request:
types: [ labeled ]
jobs:
test:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- name: Checkout workflow repo
uses: actions/checkout@v5
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests and update snapshots
id: playwright-tests
run: pnpm exec playwright test --update-snapshots
continue-on-error: true
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Debugging info
run: |
echo "Branch: ${{ github.head_ref }}"
git status
working-directory: ComfyUI_frontend
- name: Commit updated expectations
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
git add browser_tests
git commit -m "[automated] Update test expectations"
git push origin HEAD:${{ github.head_ref }}
working-directory: ComfyUI_frontend

View File

@@ -1,5 +1,4 @@
name: "CI: JSON Validation"
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
name: Validate JSON
on:
push:

View File

@@ -14,11 +14,6 @@ on:
required: false
default: ''
type: string
branch:
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
required: true
default: 'main'
type: string
jobs:
bump-version-desktop-ui:
@@ -31,25 +26,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
persist-credentials: false
- name: Validate branch exists
run: |
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
echo "Available core branches:"
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
echo ""
echo "Main branch:"
echo " - main"
exit 1
fi
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -86,10 +64,8 @@ jobs:
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: ${{ github.event.inputs.branch }}
base: main
labels: |
Release

View File

@@ -1,5 +1,4 @@
name: "Release: Version Bump"
description: "Manual workflow to increment package version with semantic versioning support"
name: Version Bump
on:
workflow_dispatch:
@@ -15,11 +14,6 @@ on:
required: false
default: ''
type: string
branch:
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
required: true
default: 'main'
type: string
jobs:
bump-version:
@@ -31,24 +25,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
- name: Validate branch exists
run: |
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
echo "Available core branches:"
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
echo ""
echo "Main branch:"
echo " - main"
exit 1
fi
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -59,6 +35,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Bump version
id: bump-version
@@ -81,9 +58,7 @@ jobs:
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: ${{ github.event.inputs.branch }}
base: main
labels: |
Release

View File

@@ -1,5 +1,4 @@
name: "CI: Tests Unit"
description: "Unit and component testing with Vitest"
name: Vitest Tests
on:
push:
@@ -7,10 +6,6 @@ on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
@@ -29,6 +24,19 @@ jobs:
node-version: "lts/*"
cache: "pnpm"
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
coverage
.vitest-cache
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
restore-keys: |
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
vitest-cache-${{ runner.os }}-
test-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile

2
.gitignore vendored
View File

@@ -78,7 +78,7 @@ templates_repo/
vite.config.mts.timestamp-*.mjs
# Linux core dumps
/core
./core
*storybook.log
storybook-static

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Run Knip with cache via package script
pnpm knip 1>&2
pnpm knip

View File

@@ -7,5 +7,12 @@
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
"overrides": [
{
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
"options": {
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
}
]
}

View File

@@ -211,17 +211,18 @@ This Storybook setup includes:
## Icon Usage in Storybook
In this project, only the `<i class="icon-[lucide--folder]" />` syntax from unplugin-icons is supported in Storybook.
In this project, the `<i-lucide:... />` syntax from unplugin-icons is not supported in Storybook.
**Example:**
```vue
<script setup lang="ts">
import { Trophy, Settings } from 'lucide-vue-next'
</script>
<template>
<i class="icon-[lucide--trophy] text-neutral size-4" />
<i class="icon-[lucide--settings] text-neutral size-4" />
<Trophy :size="16" class="text-neutral" />
<Settings :size="16" class="text-neutral" />
</template>
```

View File

@@ -7,15 +7,15 @@
}
],
"rules": {
"import-notation": "string",
"import-notation": "url",
"font-family-no-missing-generic-family-keyword": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-property-value-no-unknown": [
true,
{
"ignoreProperties": {
"speak": ["none"],
"app-region": ["drag", "no-drag"],
"/^(width|height)$/": ["/^v-bind/"]
"app-region": ["drag", "no-drag"]
}
}
],
@@ -35,7 +35,7 @@
"selector-max-type": 2,
"declaration-block-no-duplicate-properties": true,
"block-no-empty": true,
"no-descending-specificity": null,
"no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
"at-rule-no-unknown": [
true,
@@ -57,8 +57,7 @@
true,
{
"ignoreFunctions": [
"theme",
"v-bind"
"theme"
]
}
]

View File

@@ -5,7 +5,7 @@
- Routing/i18n/entry: `src/router.ts`, `src/i18n.ts`, `src/main.ts`.
- Tests: unit/component in `tests-ui/` and `src/components/**/*.{test,spec}.ts`; E2E in `browser_tests/`.
- Public assets: `public/`. Build output: `dist/`.
- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.ts`, `.prettierrc`.
- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.js`, `.prettierrc`.
## Build, Test, and Development Commands
- `pnpm dev`: Start Vite dev server.

View File

@@ -62,11 +62,6 @@ Key Nx features:
## Project Philosophy
- Follow good software engineering principles
- YAGNI
- AHA
- DRY
- SOLID
- Clean, stable public APIs
- Domain-driven design
- Thousands of users and extensions
@@ -131,5 +126,6 @@ const value = api.getServerFeature('config_name', defaultValue) // Get config
- NEVER use `--no-verify` flag when committing
- NEVER delete or disable tests to make them pass
- NEVER circumvent quality checks
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
- NEVER use `dark:` prefix - always use `dark-theme:` for dark mode styles, for example: `dark-theme:text-white dark-theme:bg-black`
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('bg-red-500', { 'bg-blue-500': condition })" />`

View File

@@ -54,10 +54,3 @@
# Translations
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
# LLM Instructions (blank on purpose)
.claude/
.cursor/
.cursorrules
**/AGENTS.md
**/CLAUDE.md

View File

@@ -43,7 +43,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
```
3. Configure environment (optional):
Create a `.env` file in the project root based on the provided [.env_example](.env_example) file.
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
@@ -255,7 +255,7 @@ pnpm format
The project supports three types of icons, all with automatic imports (no manual imports needed):
1. **PrimeIcons** - Built-in PrimeVue icons using CSS classes: `<i class="pi pi-plus" />`
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i class="icon-[lucide--settings]" />`, `<i class="icon-[mdi--folder]" />`
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i-lucide:settings />`, `<i-mdi:folder />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
@@ -325,4 +325,4 @@ If you have questions about contributing:
- Ask in our [Discord](https://discord.com/invite/comfyorg)
- Open a new issue for clarification
Thank you for contributing to ComfyUI Frontend!
Thank you for contributing to ComfyUI Frontend!

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ComfyUI</title>
<title>ComfyUI Desktop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
</head>
<body>

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/desktop-ui",
"version": "0.0.3",
"version": "0.0.1",
"type": "module",
"nx": {
"tags": [

View File

@@ -1,17 +1,17 @@
<template>
<div
class="mx-auto grid h-[40rem] w-full max-w-3xl grid-rows-[1fr_auto_auto_1fr] select-none"
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
>
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
{{ $t('install.gpuPicker.title') }}
</h2>
<!-- GPU Selection buttons - takes up remaining space and centers content -->
<div class="flex flex-1 items-center justify-center gap-8">
<div class="flex-1 flex gap-8 justify-center items-center">
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
:image-path="'./assets/images/apple-mps-logo.png'"
:image-path="'/assets/images/apple-mps-logo.png'"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
@@ -21,7 +21,7 @@
/>
<HardwareOption
v-else
:image-path="'./assets/images/nvidia-logo-square.jpg'"
:image-path="'/assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
@@ -47,17 +47,17 @@
/>
</div>
<div class="h-16 px-24 pt-12">
<div class="pt-12 px-24 h-16">
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
<Tag
:value="$t('install.gpuPicker.recommended')"
class="rounded-full bg-neutral-300 px-2 py-[1px] text-sm font-bold text-neutral-900"
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
/>
<i class="icon-[lucide--badge-check] text-lg text-neutral-300" />
<i-lucide:badge-check class="text-neutral-300 text-lg" />
</div>
</div>
<div class="px-24 text-neutral-300">
<div class="text-neutral-300 px-24">
<p v-show="descriptionText" class="leading-relaxed">
{{ descriptionText }}
</p>

View File

@@ -286,12 +286,6 @@ const onFocus = async () => {
.p-accordionheader {
@apply rounded-t-xl rounded-b-none;
}
.p-accordionheader-toggle-icon {
&::before {
content: '\e902';
}
}
}
.p-accordioncontent {
@@ -308,5 +302,13 @@ const onFocus = async () => {
content: '\e933';
}
}
.p-accordionpanel-active {
.p-accordionheader-toggle-icon {
&::before {
content: '\e902';
}
}
}
}
</style>

View File

@@ -1,23 +1,23 @@
<template>
<div
class="task-div relative grid min-h-52 max-w-48"
class="task-div max-w-48 min-h-52 grid relative"
:class="{ 'opacity-75': isLoading }"
>
<Card
class="relative h-full max-w-48 overflow-hidden"
class="max-w-48 relative h-full overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
<i
v-if="runner.state === 'error'"
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
style="font-size: 10rem"
/>
<img
v-if="task.headerImg"
:src="task.headerImg"
class="h-full w-full object-contain px-4 pt-4 opacity-25"
class="object-contain w-full h-full opacity-25 pt-4 px-4"
/>
</template>
<template #title>
@@ -27,7 +27,7 @@
{{ description }}
</template>
<template #footer>
<div class="mt-1 flex gap-4">
<div class="flex gap-4 mt-1">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
@@ -73,7 +73,7 @@ defineEmits<{
// Bindings
const description = computed(() =>
runner.value.state === 'error'
? (props.task.errorDescription ?? props.task.shortDescription)
? props.task.errorDescription ?? props.task.shortDescription
: props.task.shortDescription
)

View File

@@ -1,163 +1,67 @@
// Import only English locale eagerly as the default/fallback
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
// eslint-disable-next-line import-x/no-unresolved
import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' }
import ar from '@frontend-locales/ar/main.json' with { type: 'json' }
import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' }
import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' }
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import en from '@frontend-locales/en/main.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' }
import es from '@frontend-locales/es/main.json' with { type: 'json' }
import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' }
import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' }
import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' }
import fr from '@frontend-locales/fr/main.json' with { type: 'json' }
import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' }
import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' }
import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' }
import ja from '@frontend-locales/ja/main.json' with { type: 'json' }
import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' }
import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' }
import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' }
import ko from '@frontend-locales/ko/main.json' with { type: 'json' }
import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' }
import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' }
import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' }
import ru from '@frontend-locales/ru/main.json' with { type: 'json' }
import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' }
import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' }
import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' }
import tr from '@frontend-locales/tr/main.json' with { type: 'json' }
import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' }
import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' }
import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' }
import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' }
import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' }
import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' }
import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' }
import zh from '@frontend-locales/zh/main.json' with { type: 'json' }
import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' }
import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' }
import { createI18n } from 'vue-i18n'
function buildLocale<
M extends Record<string, unknown>,
N extends Record<string, unknown>,
C extends Record<string, unknown>,
S extends Record<string, unknown>
>(main: M, nodes: N, commands: C, settings: S) {
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
return {
...main,
nodeDefs: nodes,
commands: commands,
settings: settings
} as M & { nodeDefs: N; commands: C; settings: S }
}
// Locale loader map - dynamically import locales only when needed
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
/* eslint-disable import-x/no-unresolved */
const localeLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/main.json'),
es: () => import('@frontend-locales/es/main.json'),
fr: () => import('@frontend-locales/fr/main.json'),
ja: () => import('@frontend-locales/ja/main.json'),
ko: () => import('@frontend-locales/ko/main.json'),
ru: () => import('@frontend-locales/ru/main.json'),
tr: () => import('@frontend-locales/tr/main.json'),
zh: () => import('@frontend-locales/zh/main.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
}
const nodeDefsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/nodeDefs.json'),
es: () => import('@frontend-locales/es/nodeDefs.json'),
fr: () => import('@frontend-locales/fr/nodeDefs.json'),
ja: () => import('@frontend-locales/ja/nodeDefs.json'),
ko: () => import('@frontend-locales/ko/nodeDefs.json'),
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
}
const commandsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/commands.json'),
es: () => import('@frontend-locales/es/commands.json'),
fr: () => import('@frontend-locales/fr/commands.json'),
ja: () => import('@frontend-locales/ja/commands.json'),
ko: () => import('@frontend-locales/ko/commands.json'),
ru: () => import('@frontend-locales/ru/commands.json'),
tr: () => import('@frontend-locales/tr/commands.json'),
zh: () => import('@frontend-locales/zh/commands.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
}
const settingsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/settings.json'),
es: () => import('@frontend-locales/es/settings.json'),
fr: () => import('@frontend-locales/fr/settings.json'),
ja: () => import('@frontend-locales/ja/settings.json'),
ko: () => import('@frontend-locales/ko/settings.json'),
ru: () => import('@frontend-locales/ru/settings.json'),
tr: () => import('@frontend-locales/tr/settings.json'),
zh: () => import('@frontend-locales/zh/settings.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
}
// Track which locales have been loaded
const loadedLocales = new Set<string>(['en'])
// Track locales currently being loaded to prevent race conditions
const loadingLocales = new Map<string, Promise<void>>()
/**
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
*/
export async function loadLocale(locale: string): Promise<void> {
if (loadedLocales.has(locale)) {
return
}
// If already loading, return the existing promise to prevent duplicate loads
const existingLoad = loadingLocales.get(locale)
if (existingLoad) {
return existingLoad
}
const loader = localeLoaders[locale]
const nodeDefsLoader = nodeDefsLoaders[locale]
const commandsLoader = commandsLoaders[locale]
const settingsLoader = settingsLoaders[locale]
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
console.warn(`Locale "${locale}" is not supported`)
return
}
// Create and track the loading promise
const loadPromise = (async () => {
try {
const [main, nodes, commands, settings] = await Promise.all([
loader(),
nodeDefsLoader(),
commandsLoader(),
settingsLoader()
])
const messages = buildLocale(
main.default,
nodes.default,
commands.default,
settings.default
)
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
loadedLocales.add(locale)
} catch (error) {
console.error(`Failed to load locale "${locale}":`, error)
throw error
} finally {
// Clean up the loading promise once complete
loadingLocales.delete(locale)
}
})()
loadingLocales.set(locale, loadPromise)
return loadPromise
}
// Only include English in the initial bundle
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings)
en: buildLocale(en, enNodes, enCommands, enSettings),
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings),
tr: buildLocale(tr, trNodes, trCommands, trSettings)
}
// Type for locale messages - inferred from the English locale structure
type LocaleMessages = typeof messages.en
export const i18n = createI18n({
// Must set `false`, as Vue I18n Legacy API is for Vue 2
legacy: false,

View File

@@ -65,12 +65,12 @@ onUnmounted(() => electron.Validation.dispose())
.download-bg::before {
@apply m-0 absolute text-muted;
font-family: 'primeicons', sans-serif;
font-family: 'primeicons';
top: -2rem;
right: 2rem;
speak: none;
font-style: normal;
font-weight: 400;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;

View File

@@ -186,12 +186,12 @@ onUnmounted(() => electron.Validation.dispose())
.backspan::before {
@apply m-0 absolute text-muted;
font-family: 'primeicons', sans-serif;
font-family: 'primeicons';
top: -2rem;
right: -2rem;
speak: none;
font-style: normal;
font-weight: 400;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;

View File

@@ -18,16 +18,16 @@
style="
background: radial-gradient(
ellipse 800px 600px at center,
rgb(23 23 23 / 0.95) 0%,
rgb(23 23 23 / 0.93) 10%,
rgb(23 23 23 / 0.9) 20%,
rgb(23 23 23 / 0.85) 30%,
rgb(23 23 23 / 0.75) 40%,
rgb(23 23 23 / 0.6) 50%,
rgb(23 23 23 / 0.4) 60%,
rgb(23 23 23 / 0.2) 70%,
rgb(23 23 23 / 0.1) 80%,
rgb(23 23 23 / 0.05) 90%,
rgba(23, 23, 23, 0.95) 0%,
rgba(23, 23, 23, 0.93) 10%,
rgba(23, 23, 23, 0.9) 20%,
rgba(23, 23, 23, 0.85) 30%,
rgba(23, 23, 23, 0.75) 40%,
rgba(23, 23, 23, 0.6) 50%,
rgba(23, 23, 23, 0.4) 60%,
rgba(23, 23, 23, 0.2) 70%,
rgba(23, 23, 23, 0.1) 80%,
rgba(23, 23, 23, 0.05) 90%,
transparent 100%
);
"
@@ -66,6 +66,17 @@
@click="troubleshoot"
/>
</div>
<div class="text-center">
<button
v-if="!terminalVisible"
class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-2 mx-auto"
@click="terminalVisible = true"
>
<i class="pi pi-search"></i>
{{ $t('serverStart.showTerminal') }}
</button>
</div>
</div>
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->

View File

@@ -1,90 +0,0 @@
{
"id": "95ea19ba-456c-46e8-aa40-dc3ff135b746",
"revision": 0,
"last_node_id": 11,
"last_link_id": 10,
"nodes": [
{
"id": 10,
"type": "KSampler",
"pos": [494.3333740234375, 142.3333282470703],
"size": [444, 399],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
},
{
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": 10
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [67, "randomize", 20, 8, "euler", "simple", 1]
},
{
"id": 11,
"type": "PrimitiveInt",
"pos": [24.333343505859375, 149.6666717529297],
"size": [444, 125],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [10]
}
],
"properties": {
"Node name for S&R": "PrimitiveInt"
},
"widgets_values": [67, "randomize"]
}
],
"links": [[10, 11, 0, 10, 4, "INT"]],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
},
"frontendVersion": "1.28.6"
},
"version": 0.4
}

View File

@@ -1,5 +1,6 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { test as base, expect } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
@@ -46,10 +47,6 @@ class ComfyMenu {
.nth(0)
}
get buttons() {
return this.sideToolbar.locator('.side-bar-button')
}
get nodeLibraryTab() {
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab
@@ -133,8 +130,7 @@ export class ComfyPage {
// Buttons
public readonly resetViewButton: Locator
public readonly queueButton: Locator // Run button in Legacy UI
public readonly runButton: Locator // Run button (renamed "Queue" -> "Run")
public readonly queueButton: Locator
// Inputs
public readonly workflowUploadInput: Locator
@@ -169,9 +165,6 @@ export class ComfyPage {
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.runButton = page
.getByTestId('queue-button')
.getByRole('button', { name: 'Run' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.visibleToasts = page.locator('.p-toast-message:visible')
@@ -1093,6 +1086,12 @@ export class ComfyPage {
const targetPosition = await targetSlot.getPosition()
// Debug: Log the positions we're trying to use
console.log('Drag positions:', {
source: sourcePosition,
target: targetPosition
})
await this.dragAndDrop(sourcePosition, targetPosition)
await this.nextFrame()
}

View File

@@ -3,8 +3,6 @@
*/
import type { Locator, Page } from '@playwright/test'
import { VueNodeFixture } from './utils/vueNodeFixtures'
export class VueNodeHelpers {
constructor(private page: Page) {}
@@ -26,7 +24,9 @@ export class VueNodeHelpers {
* Get locator for selected Vue node components (using visual selection indicators)
*/
get selectedNodes(): Locator {
return this.page.locator('[data-node-id].outline-node-component-outline')
return this.page.locator(
'[data-node-id].outline-black, [data-node-id].outline-white'
)
}
/**
@@ -108,24 +108,6 @@ export class VueNodeHelpers {
await this.page.keyboard.press('Backspace')
}
/**
* Return a DOM-focused VueNodeFixture for the first node matching the title.
* Resolves the node id up front so subsequent interactions survive title changes.
*/
async getFixtureByTitle(title: string): Promise<VueNodeFixture> {
const node = this.getNodeByTitle(title).first()
await node.waitFor({ state: 'visible' })
const nodeId = await node.evaluate((el) => el.getAttribute('data-node-id'))
if (!nodeId) {
throw new Error(
`Vue node titled "${title}" is missing its data-node-id attribute`
)
}
return new VueNodeFixture(this.getNodeLocator(nodeId))
}
/**
* Wait for Vue nodes to be rendered
*/
@@ -139,24 +121,4 @@ export class VueNodeHelpers {
await this.page.waitForSelector('[data-node-id]')
}
}
/**
* Get a specific widget by node title and widget name
*/
getWidgetByName(nodeTitle: string, widgetName: string): Locator {
return this.getNodeByTitle(nodeTitle).locator(
`_vue=[widget.name="${widgetName}"]`
)
}
/**
* Get controls for input number widgets (increment/decrement buttons and input)
*/
getInputNumberControls(widget: Locator) {
return {
input: widget.locator('input'),
incrementButton: widget.locator('button').first(),
decrementButton: widget.locator('button').last()
}
}
}

View File

@@ -7,7 +7,7 @@ export class Topbar {
constructor(public readonly page: Page) {
this.menuLocator = page.locator('.comfy-command-menu')
this.menuTrigger = page.locator('.comfy-menu-button-wrapper')
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
}
async getTabNames(): Promise<string[]> {
@@ -105,7 +105,7 @@ export class Topbar {
* Close the topbar menu by clicking outside
*/
async closeTopbarMenu() {
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
await expect(this.menuLocator).not.toBeVisible()
}

View File

@@ -1,66 +1,131 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
/** DOM-centric helper for a single Vue-rendered node on the canvas. */
import type { NodeReference } from './litegraphUtils'
/**
* VueNodeFixture provides Vue-specific testing utilities for interacting with
* Vue node components. It bridges the gap between litegraph node references
* and Vue UI components.
*/
export class VueNodeFixture {
constructor(private readonly locator: Locator) {}
constructor(
private readonly nodeRef: NodeReference,
private readonly page: Page
) {}
get header(): Locator {
return this.locator.locator('[data-testid^="node-header-"]')
/**
* Get the node's header element using data-testid
*/
async getHeader(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
}
get title(): Locator {
return this.locator.locator('[data-testid="node-title"]')
}
get titleInput(): Locator {
return this.locator.locator('[data-testid="node-title-input"]')
}
get body(): Locator {
return this.locator.locator('[data-testid^="node-body-"]')
}
get collapseButton(): Locator {
return this.locator.locator('[data-testid="node-collapse-button"]')
}
get collapseIcon(): Locator {
return this.collapseButton.locator('i')
}
get root(): Locator {
return this.locator
/**
* Get the node's title element
*/
async getTitleElement(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-title"]')
}
/**
* Get the current title text
*/
async getTitle(): Promise<string> {
return (await this.title.textContent()) ?? ''
const titleElement = await this.getTitleElement()
return (await titleElement.textContent()) || ''
}
async setTitle(value: string): Promise<void> {
await this.header.dblclick()
const input = this.titleInput
await expect(input).toBeVisible()
await input.fill(value)
/**
* Set a new title by double-clicking and entering text
*/
async setTitle(newTitle: string): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()
const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill(newTitle)
await input.press('Enter')
}
/**
* Cancel title editing
*/
async cancelTitleEdit(): Promise<void> {
await this.header.dblclick()
const input = this.titleInput
await expect(input).toBeVisible()
const titleElement = await this.getTitleElement()
await titleElement.dblclick()
const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.press('Escape')
}
/**
* Check if the title is currently being edited
*/
async isEditingTitle(): Promise<boolean> {
const header = await this.getHeader()
const input = header.locator('[data-testid="node-title-input"]')
return await input.isVisible()
}
/**
* Get the collapse/expand button
*/
async getCollapseButton(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-collapse-button"]')
}
/**
* Toggle the node's collapsed state
*/
async toggleCollapse(): Promise<void> {
await this.collapseButton.click()
const button = await this.getCollapseButton()
await button.click()
}
/**
* Get the collapse icon element
*/
async getCollapseIcon(): Promise<Locator> {
const button = await this.getCollapseButton()
return button.locator('i')
}
/**
* Get the collapse icon's CSS classes
*/
async getCollapseIconClass(): Promise<string> {
return (await this.collapseIcon.getAttribute('class')) ?? ''
const icon = await this.getCollapseIcon()
return (await icon.getAttribute('class')) || ''
}
boundingBox(): ReturnType<Locator['boundingBox']> {
return this.locator.boundingBox()
/**
* Check if the collapse button is visible
*/
async isCollapseButtonVisible(): Promise<boolean> {
const button = await this.getCollapseButton()
return await button.isVisible()
}
/**
* Get the node's body/content element
*/
async getBody(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
}
/**
* Check if the node body is visible (not collapsed)
*/
async isBodyVisible(): Promise<boolean> {
const body = await this.getBody()
return await body.isVisible()
}
}

View File

@@ -13,7 +13,7 @@ export class ComfyActionbar {
async isDocked() {
const className = await this.root.getAttribute('class')
return className?.includes('static') ?? false
return className?.includes('is-docked') ?? false
}
}

View File

@@ -116,10 +116,9 @@ test.describe('Actionbar', () => {
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
await comfyPage.page.dragAndDrop(
'.actionbar .drag-handle',
'.actionbar-container',
'.comfyui-menu',
{
targetPosition: { x: 50, y: 20 },
force: true
targetPosition: { x: 0, y: 0 }
}
)
expect(await comfyPage.actionbar.isDocked()).toBe(true)

View File

@@ -301,9 +301,7 @@ test.describe('Settings', () => {
})
test.describe('Support', () => {
test('Should open external zendesk link with OSS tag', async ({
comfyPage
}) => {
test('Should open external zendesk link', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
@@ -311,10 +309,6 @@ test.describe('Support', () => {
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
const url = new URL(newPage.url())
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
await newPage.close()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -39,15 +39,15 @@ test.describe('Graph Canvas Menu', () => {
)
})
test('Toggle minimap button is clickable and has correct test id', async ({
test('Focus mode button is clickable and has correct test id', async ({
comfyPage
}) => {
const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapButton).toBeVisible()
await expect(minimapButton).toBeEnabled()
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
await expect(focusButton).toBeVisible()
await expect(focusButton).toBeEnabled()
// Test that the button can be clicked without error
await minimapButton.click()
await focusButton.click()
await comfyPage.nextFrame()
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -233,7 +233,6 @@ test.describe('Group Node', () => {
}
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
await comfyPage.menu.nodeLibraryTab.open()
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
.getFolder(GROUP_NODE_CATEGORY)
.count()
@@ -254,6 +253,8 @@ test.describe('Group Node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.loadWorkflow(WORKFLOW_NAME)
await comfyPage.menu.nodeLibraryTab.open()
groupNode = await comfyPage.getFirstNodeRef()
if (!groupNode)
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)

View File

@@ -3,10 +3,10 @@ import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import {
type ComfyPage,
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
@@ -786,25 +786,24 @@ test.describe('Viewport settings', () => {
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await toggleButton.click()
// close zoom menu
await zoomControlsButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
// Save workflow as a new file, then zoom out before screen shot
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
await comfyPage.nextFrame()
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
await changeTab(tabA)
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
await changeTab(tabB)
await comfyMouse.move(comfyPage.emptySpace)
for (let i = 0; i < 4; i++) {
await comfyMouse.wheel(0, 60)
@@ -816,6 +815,9 @@ test.describe('Viewport settings', () => {
// Ensure that the screenshots are different due to zoom level
expect(screenshotB).not.toBe(screenshotA)
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
// Go back to Workflow A
await changeTab(tabA)
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -8,7 +8,9 @@ test.describe('Menu', () => {
})
test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.buttons.count()
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)
await comfyPage.page.evaluate(async () => {
window['app'].extensionManager.registerSidebarTab({
@@ -24,7 +26,9 @@ test.describe('Menu', () => {
})
await comfyPage.nextFrame()
const newChildrenCount = await comfyPage.menu.buttons.count()
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)
expect(newChildrenCount).toBe(initialChildrenCount + 1)
})

View File

@@ -35,6 +35,12 @@ test.describe('Minimap', () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(toggleButton).toBeVisible()
@@ -45,6 +51,13 @@ test.describe('Minimap', () => {
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapContainer).toBeVisible()
@@ -53,11 +66,13 @@ test.describe('Minimap', () => {
await comfyPage.nextFrame()
await expect(minimapContainer).not.toBeVisible()
await expect(toggleButton).toContainText('Show Minimap')
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toContainText('Hide Minimap')
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -1,28 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Record Audio Node', () => {
test('should add a record audio node and take a screenshot', async ({
comfyPage
}) => {
// Open the search box by double clicking on the canvas
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
// Search for and add the RecordAudio node
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
await comfyPage.nextFrame()
// Verify the RecordAudio node was added
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
expect(recordAudioNodes.length).toBe(1)
// Take a screenshot of the canvas with the RecordAudio node
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -8,7 +8,6 @@ const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)
await comfyPage.vueNodes.waitForNodes()
})
@@ -16,7 +15,6 @@ test.describe('Vue Node Groups', () => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-create-group.png'
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -60,6 +60,7 @@ async function getInputLinkDetails(
)
}
// Test helpers to reduce repetition across cases
function slotLocator(
page: Page,
nodeId: NodeId,
@@ -787,210 +788,4 @@ test.describe('Vue Node Link Interaction', () => {
targetSlot: 2
})
})
test('should batch disconnect all links with ctrl+alt+click on slot', async ({
comfyPage
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 1 },
() => comfyPage.nextFrame()
)
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 2 },
() => comfyPage.nextFrame()
)
const clipOutput = await clipNode.getOutput(0)
expect(await clipOutput.getLinkCount()).toBe(2)
const clipOutputSlot = slotLocator(comfyPage.page, clipNode.id, 0, false)
await clipOutputSlot.dispatchEvent('pointerdown', {
button: 0,
buttons: 1,
ctrlKey: true,
altKey: true,
shiftKey: false,
bubbles: true,
cancelable: true
})
await comfyPage.nextFrame()
expect(await clipOutput.getLinkCount()).toBe(0)
})
test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dropPos = { x: outputCenter.x + 180, y: outputCenter.y - 140 }
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
// Context menu should be visible
const contextMenu = comfyPage.page.locator('.litecontextmenu')
await expect(contextMenu).toBeVisible()
// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()
// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
})
test('Context menu -> Search pre-filters by link type and connects after selection', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dropPos = { x: outputCenter.x + 200, y: outputCenter.y - 120 }
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
// Open Search from the context menu
await comfyPage.clickContextMenuItem('Search')
// Search box opens with prefilled type filter based on link type (LATENT)
await expect(comfyPage.searchBox.input).toBeVisible()
const chips = comfyPage.searchBox.filterChips
// Ensure at least one filter chip exists and it matches the link type
const chipCount = await chips.count()
expect(chipCount).toBeGreaterThan(0)
await expect(chips.first()).toContainText('LATENT')
// Choose a compatible node and verify it auto-connects
await comfyPage.searchBox.fillAndSelectFirstNode('VAEDecode')
await comfyPage.nextFrame()
// KSampler output should now have an outgoing link
const samplerOutput = await samplerNode.getOutput(0)
expect(await samplerOutput.getLinkCount()).toBe(1)
// One of the VAEDecode nodes should have an incoming link on input[0]
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
let linked = false
for (const vae of vaeNodes) {
const details = await getInputLinkDetails(comfyPage.page, vae.id, 0)
if (details) {
expect(details.originId).toBe(samplerNode.id)
linked = true
break
}
}
expect(linked).toBe(true)
})
test('Search box opens on Shift-drop and connects after selection', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dropPos = { x: outputCenter.x + 140, y: outputCenter.y - 100 }
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
// Search box should open directly
await expect(comfyPage.searchBox.input).toBeVisible()
await expect(comfyPage.searchBox.filterChips.first()).toContainText(
'LATENT'
)
// Select a compatible node and verify connection
await comfyPage.searchBox.fillAndSelectFirstNode('VAEDecode')
await comfyPage.nextFrame()
const samplerOutput = await samplerNode.getOutput(0)
expect(await samplerOutput.getLinkCount()).toBe(1)
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
let linked = false
for (const vae of vaeNodes) {
const details = await getInputLinkDetails(comfyPage.page, vae.id, 0)
if (details) {
expect(details.originId).toBe(samplerNode.id)
linked = true
break
}
}
expect(linked).toBe(true)
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -2,46 +2,70 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
test.describe('Vue Nodes Renaming', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.vueNodes.waitForNodes()
})
test('should display node title', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.header).toContainText('KSampler')
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
expect(nodes.length).toBeGreaterThanOrEqual(1)
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
const title = await vueNode.getTitle()
expect(title).toBe('KSampler')
// Verify title is visible in the header
const header = await vueNode.getHeader()
await expect(header).toContainText('KSampler')
})
test('should allow title renaming by double clicking on the node header', async ({
comfyPage
}) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Test renaming with Enter
await vueNode.setTitle('My Custom Sampler')
await expect(await vueNode.getTitle()).toBe('My Custom Sampler')
await expect(vueNode.header).toContainText('My Custom Sampler')
const newTitle = await vueNode.getTitle()
expect(newTitle).toBe('My Custom Sampler')
// Verify the title is displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('My Custom Sampler')
// Test cancel with Escape
await vueNode.title.dblclick()
const titleElement = await vueNode.getTitleElement()
await titleElement.dblclick()
await comfyPage.nextFrame()
await vueNode.titleInput.fill('This Should Be Cancelled')
await vueNode.titleInput.press('Escape')
// Type a different value but cancel
const input = (await vueNode.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill('This Should Be Cancelled')
await input.press('Escape')
await comfyPage.nextFrame()
// Title should remain as the previously saved value
await expect(await vueNode.getTitle()).toBe('My Custom Sampler')
const titleAfterCancel = await vueNode.getTitle()
expect(titleAfterCancel).toBe('My Custom Sampler')
})
test('Double click node body does not trigger edit', async ({
comfyPage
}) => {
const loadCheckpointNode = comfyPage.vueNodes
.getNodeByTitle('Load Checkpoint')
.first()
const loadCheckpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBbox = await loadCheckpointNode.boundingBox()
if (!nodeBbox) throw new Error('Node not found')
await loadCheckpointNode.dblclick()

View File

@@ -49,44 +49,4 @@ test.describe('Vue Node Selection', () => {
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
}
test('should select all nodes with ctrl+a', async ({ comfyPage }) => {
const initialCount = await comfyPage.vueNodes.getNodeCount()
expect(initialCount).toBeGreaterThan(0)
await comfyPage.canvas.press('Control+a')
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
expect(selectedCount).toBe(initialCount)
})
test('should select pinned node without dragging', async ({ comfyPage }) => {
const PIN_HOTKEY = 'p'
const PIN_INDICATOR = '[data-testid="node-pin-indicator"]'
const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint')
await checkpointNodeHeader.click()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const pinIndicator = checkpointNode.locator(PIN_INDICATOR)
await expect(pinIndicator).toBeVisible()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
const initialPos = await checkpointNodeHeader.boundingBox()
if (!initialPos) throw new Error('Failed to get header position')
await comfyPage.dragAndDrop(
{ x: initialPos.x + 10, y: initialPos.y + 10 },
{ x: initialPos.x + 100, y: initialPos.y + 100 }
)
const finalPos = await checkpointNodeHeader.boundingBox()
if (!finalPos) throw new Error('Failed to get header position after drag')
expect(finalPos).toEqual(initialPos)
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
})
})

View File

@@ -9,9 +9,6 @@ const BYPASS_CLASS = /before:bg-bypass\/60/
test.describe('Vue Node Bypass', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.vueNodes.waitForNodes()
})
@@ -23,10 +20,6 @@ test.describe('Vue Node Bypass', () => {
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)

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