Compare commits

..

1 Commits

Author SHA1 Message Date
Terry Jia
2c33e21390 allow widget expand to fill available vertical space 2025-10-23 20:35:08 -04:00
624 changed files with 34946 additions and 51523 deletions

View File

@@ -23,10 +23,10 @@ TEST_COMFYUI_DIR=/home/ComfyUI
# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true
# Whether to disable proxying the `/templates` route. If true, allows you to
# serve templates from the ComfyUI_frontend/public/templates folder (for
# locally testing changes to templates). When false or nonexistent, the
# templates are served via the normal method from the server's python site
# Whether to disable proxying the `/templates` route. If true, allows you to
# serve templates from the ComfyUI_frontend/public/templates folder (for
# locally testing changes to templates). When false or nonexistent, the
# templates are served via the normal method from the server's python site
# packages.
DISABLE_TEMPLATES_PROXY=false
@@ -37,8 +37,3 @@ DISABLE_VUE_PLUGINS=false
# Algolia credentials required for developing with the new custom node manager.
ALGOLIA_APP_ID=4E0RO38HS8
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
# Sentry ENV vars replace with real ones for debugging
# SENTRY_AUTH_TOKEN=private-token # get from sentry
# SENTRY_ORG=comfy-org
# SENTRY_PROJECT=cloud-frontend-staging

View File

@@ -25,6 +25,3 @@ e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
# [refactor] Improve updates/notifications domain organization (#5590)
27ab355f9c73415dc39f4d3f512b02308f847801
# Migrate Tailwind styles to design-system package
9f19d8fb4bd22518879343b49c05634dca777df0

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,51 +0,0 @@
name: "CI: Tests E2E Cloud"
description: "Cloud E2E testing with Playwright against stagingcloud.comfy.org"
on:
workflow_dispatch:
push:
branches: [cloud/*, main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
playwright-tests-cloud:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: false # Cloud tests don't need build
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright cloud tests
id: playwright
env:
CLOUD_TEST_EMAIL: ${{ secrets.CLOUD_TEST_EMAIL }}
CLOUD_TEST_PASSWORD: ${{ secrets.CLOUD_TEST_PASSWORD }}
run: |
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --config=playwright.cloud.config.ts \
--reporter=list \
--reporter=html \
--reporter=json
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-cloud
path: ./playwright-report/
retention-days: 30

View File

@@ -69,7 +69,34 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- 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: |
# 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: Collect backport targets
if: steps.check-existing.outputs.skip != 'true'
id: targets
run: |
TARGETS=()
@@ -111,14 +138,6 @@ jobs:
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
@@ -132,76 +151,8 @@ jobs:
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")
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
fi
- 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,7 +170,7 @@ jobs:
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
for target in ${{ steps.targets.outputs.targets }}; do
TARGET_BRANCH="${target}"
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
@@ -234,14 +185,6 @@ jobs:
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 "
echo "::endgroup::"
continue
fi
# Create backport branch
git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
@@ -276,7 +219,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 }}
@@ -315,7 +258,7 @@ jobs:
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: |
@@ -336,9 +279,6 @@ jobs:
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."
elif [ "${reason}" = "conflicts" ]; then
# Convert comma-separated conflicts back to newlines for display
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
@@ -347,9 +287,3 @@ jobs:
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

@@ -17,9 +17,39 @@ concurrency:
cancel-in-progress: true
jobs:
claude-review:
wait-for-ci:
runs-on: ubuntu-latest
if: github.event.label.name == 'claude-review'
outputs:
should-proceed: ${{ steps.check-status.outputs.proceed }}
steps:
- name: Wait for other CI checks
uses: lewagon/wait-on-check-action@e106e5c43e8ca1edea6383a39a01c5ca495fd812
with:
ref: ${{ github.event.pull_request.head.sha }}
check-regexp: '^(lint-and-format|test|playwright-tests)'
wait-interval: 30
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if we should proceed
id: check-status
run: |
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
echo "Some CI checks failed - skipping Claude review"
echo "proceed=false" >> $GITHUB_OUTPUT
else
echo "All CI checks passed - proceeding with Claude review"
echo "proceed=true" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
claude-review:
needs: wait-for-ci
if: needs.wait-for-ci.outputs.should-proceed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository

View File

@@ -120,57 +120,13 @@ jobs:
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
# Upload updated snapshots from this shard
- name: Upload updated snapshots
uses: actions/upload-artifact@v4
if: steps.changed-snapshots.outputs.has-changes == 'true'
if: always()
with:
name: snapshots-shard-${{ matrix.shardIndex }}
path: /tmp/changed_snapshots_shard/
path: browser_tests/**/*-snapshots/
retention-days: 1
- name: Upload test report
@@ -191,31 +147,20 @@ jobs:
with:
ref: ${{ needs.setup.outputs.branch }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts
# Download all snapshot artifacts from shards
- name: Download all snapshots
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
# Merge snapshots from all shards into browser_tests directory
- name: Merge snapshots
run: |
set -euo pipefail
echo "=========================================="
echo "MERGING CHANGED SNAPSHOTS"
echo "=========================================="
echo "Merging snapshots from all shards..."
# Verify target directory exists
if [ ! -d "browser_tests" ]; then
@@ -223,79 +168,73 @@ jobs:
exit 1
fi
# List all downloaded artifacts for debugging
echo "Available shard artifacts:"
ls -la ./downloaded-snapshots/ || echo "No downloaded-snapshots directory found"
merged_count=0
skipped_count=0
for shard_dir in ./downloaded-snapshots/snapshots-shard-*; do
echo "Processing: $shard_dir"
# Check if glob matched any directories
if [ ! -e "$shard_dir" ]; then
echo "::warning::No shard directories found matching pattern"
break
fi
# For each shard's changed files, copy them directly
for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do
if [ ! -d "$shard_dir" ]; then
echo "::warning::Expected directory but found file: $shard_dir"
skipped_count=$((skipped_count + 1))
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"
# Check if directory has content (empty is OK - shard might have no changes)
if [ -z "$(ls -A "$shard_dir" 2>/dev/null)" ]; then
echo "Shard directory $shard_dir is empty (no snapshots updated in this shard)"
skipped_count=$((skipped_count + 1))
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/^/ /'
echo "Merging snapshots from $shard_dir..."
# The artifact path 'browser_tests/**/*-snapshots/' uploads relative to browser_tests/
# So downloaded artifacts contain 'tests/' directly, not 'browser_tests/tests/'
# We need to merge the artifact contents into browser_tests/
if ! rsync -a "$shard_dir/" browser_tests/; then
echo "::error::Failed to merge snapshots from $shard_dir/ to browser_tests/"
echo "Contents of $shard_dir:"
find "$shard_dir" -type f | head -20
exit 1
fi
merged_count=$((merged_count + 1))
echo " ✓ Merged"
echo ""
echo "Successfully merged shard $merged_count"
done
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: $merged_count"
echo "Snapshot merge complete. Merged: $merged_count shard(s), Skipped: $skipped_count shard(s)"
- name: Show changes
- name: Debugging info
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"
echo "PR: ${{ needs.setup.outputs.pr-number }}"
echo "Branch: ${{ needs.setup.outputs.branch }}"
git status
- 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
git add browser_tests
if git diff --cached --quiet; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
else
git commit -m "[automated] Update test expectations"
git push origin ${{ needs.setup.outputs.branch }}
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'
if: github.event_name == 'issue_comment'
with:
comment-id: ${{ needs.setup.outputs.comment-id }}
issue-number: ${{ needs.setup.outputs.pr-number }}

View File

@@ -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: "CI: Size Data"
name: size data
on:
push:

View File

@@ -1,8 +1,8 @@
name: "PR: Size Report"
name: size report
on:
workflow_run:
workflows: ['CI: Size Data']
workflows: ['size data']
types:
- completed
workflow_dispatch:
@@ -22,7 +22,7 @@ permissions:
issues: write
jobs:
comment:
size-report:
runs-on: ubuntu-latest
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
@@ -78,7 +78,7 @@ jobs:
uses: dawidd6/action-download-artifact@v11
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: ci-size-data.yaml
workflow: size-data.yml
event: push
name: size-data
path: temp/size-prev

View File

@@ -1,144 +0,0 @@
name: "Weekly Documentation Check"
description: "Automated weekly documentation accuracy check and update via Claude"
permissions:
contents: write
pull-requests: write
id-token: write
on:
schedule:
# Run every Monday at 9 AM UTC
- cron: '0 9 * * 1'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
docs-check:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: main
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies for analysis tools
run: |
# Check if packages are already available locally
if ! pnpm list typescript @vue/compiler-sfc >/dev/null 2>&1; then
echo "Installing TypeScript and Vue compiler globally..."
pnpm install -g typescript @vue/compiler-sfc
else
echo "TypeScript and Vue compiler already available locally"
fi
- name: Run Claude Documentation Review
uses: anthropics/claude-code-action@v1.0.6
with:
prompt: |
Is all documentation still 100% accurate?
INSTRUCTIONS:
1. Fact-check all documentation against the current codebase
2. Look for:
- Outdated API references
- Deprecated functions or components still documented
- Missing documentation for new features
- Incorrect code examples
- Broken internal references
- Configuration examples that no longer work
- Documentation that contradicts actual implementation
3. Update any inaccurate or outdated documentation
4. Add documentation for significant undocumented features
5. Ensure all code examples are valid and tested against current code
Focus on these key areas:
- docs/**/*.md (all documentation files)
- CLAUDE.md (project guidelines)
- README.md files throughout the repository
- .claude/commands/*.md (Claude command documentation)
Make changes directly to the documentation files as needed.
DO NOT modify any source code files unless absolutely necessary for documentation accuracy.
After making all changes, create a comprehensive PR message summary:
1. Write a detailed PR body to /tmp/pr-body-${{ github.run_id }}.md in markdown format
2. Include:
- ## Summary section with bullet points of what was changed
- ## Changes Made section with details organized by category
- ## Review Notes section with any important context
3. Be specific about which files were updated and why
4. If no changes were needed, write a brief message stating documentation is up to date
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--max-turns 256 --allowedTools 'Bash(git status),Bash(git diff),Bash(git log),Bash(pnpm:*),Bash(npm:*),Bash(node:*),Bash(tsc:*),Bash(echo:*),Read,Write,Edit,Glob,Grep'"
continue-on-error: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for changes
id: check_changes
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No documentation changes needed"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Documentation changes detected"
fi
- name: Create default PR body if not generated
if: steps.check_changes.outputs.has_changes == 'true'
run: |
if [ ! -f /tmp/pr-body-${{ github.run_id }}.md ]; then
cat > /tmp/pr-body-${{ github.run_id }}.md <<'EOF'
## Automated Documentation Review
This PR contains documentation updates identified by the weekly automated review.
### Review Process
- Automated fact-checking against current codebase
- Verification of code examples and API references
- Detection of outdated or missing documentation
### What was checked
- All markdown documentation in `docs/`
- Project guidelines in `CLAUDE.md`
- README files throughout the repository
- Claude command documentation in `.claude/commands/`
**Note**: This is an automated PR. Please review all changes carefully before merging.
🤖 Generated by weekly documentation check workflow
EOF
fi
- name: Create or Update Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'docs: weekly documentation accuracy update'
branch: docs/weekly-update
delete-branch: true
title: 'docs: Weekly Documentation Update'
body-path: /tmp/pr-body-${{ github.run_id }}.md
labels: |
documentation
automated
draft: true

5
.gitignore vendored
View File

@@ -18,7 +18,6 @@ yarn.lock
.stylelintcache
node_modules
.pnpm-store
dist
dist-ssr
*.local
@@ -58,7 +57,6 @@ coverage/
/playwright/.cache/
browser_tests/**/*-win32.png
browser_tests/local/
browser_tests/.auth/
.env
@@ -94,6 +92,3 @@ storybook-static
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*
# Weekly docs check output
/output.txt

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

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

@@ -140,7 +140,7 @@ When writing new tests, follow these patterns:
```typescript
// Import the test fixture
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Feature Name', () => {
// Set up test environment if needed

View File

@@ -1,44 +0,0 @@
import type { APIRequestContext, Page } from '@playwright/test'
import { ComfyPage } from './ComfyPage'
import type { FolderStructure } from './ComfyPage'
/**
* Cloud-specific implementation of ComfyPage.
* Uses Firebase auth persistence and cloud API for settings.
*/
export class CloudComfyPage extends ComfyPage {
constructor(
page: Page,
request: APIRequestContext,
parallelIndex: number = 0
) {
super(page, request, parallelIndex)
}
async setupUser(username: string): Promise<string | null> {
// No-op for cloud - user already authenticated via Firebase in globalSetup
// Firebase auth is persisted via storageState in the fixture
return null
}
async setupSettings(settings: Record<string, any>): Promise<void> {
// Cloud uses batch settings API (not devtools)
// Firebase auth token is automatically included from restored localStorage
const resp = await this.request.post(`${this.url}/api/settings`, {
data: settings
})
if (!resp.ok()) {
throw new Error(`Failed to setup cloud settings: ${await resp.text()}`)
}
}
async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
// Cloud workflow API not yet implemented
// For initial smoke tests, we can skip this functionality
console.warn(
'setupWorkflowsDirectory: not yet implemented for cloud mode - skipping'
)
}
}

View File

@@ -1,5 +1,5 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base, expect } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
@@ -7,8 +7,10 @@ import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { SettingDialog } from './components/SettingDialog'
@@ -92,7 +94,7 @@ class ComfyMenu {
}
}
export type FolderStructure = {
type FolderStructure = {
[key: string]: FolderStructure | string
}
@@ -120,11 +122,7 @@ class ConfirmDialog {
}
}
/**
* Abstract base class for ComfyUI page objects.
* Subclasses must implement backend-specific methods for different environments (localhost, cloud, etc.)
*/
export abstract class ComfyPage {
export class ComfyPage {
private _history: TaskHistory | null = null
public readonly url: string
@@ -158,13 +156,12 @@ export abstract class ComfyPage {
/** Test user ID for the current context */
get id() {
return this.userIds[this.parallelIndex]
return this.userIds[comfyPageFixture.info().parallelIndex]
}
constructor(
public readonly page: Page,
public readonly request: APIRequestContext,
public readonly parallelIndex: number = 0
public readonly request: APIRequestContext
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
@@ -218,24 +215,65 @@ export abstract class ComfyPage {
})
}
/**
* Setup workflows directory structure. Implementation varies by environment.
* @param structure - Folder structure to create
*/
abstract setupWorkflowsDirectory(structure: FolderStructure): Promise<void>
async setupWorkflowsDirectory(structure: FolderStructure) {
const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.id}/workflows`
}
}
)
/**
* Setup user for testing. Implementation varies by environment.
* @param username - Username to setup
* @returns User ID or null if not applicable
*/
abstract setupUser(username: string): Promise<string | null>
if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
/**
* Setup settings for testing. Implementation varies by environment.
* @param settings - Settings object to apply
*/
abstract setupSettings(settings: Record<string, any>): Promise<void>
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}
async setupUser(username: string) {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)
const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]
return id ? id : await this.createUser(username)
}
async createUser(username: string) {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})
if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)
return await resp.json()
}
async setupSettings(settings: Record<string, any>) {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
{
data: settings
}
)
if (resp.status() !== 200) {
throw new Error(`Failed to setup settings: ${await resp.text()}`)
}
}
setupHistory(): TaskHistory {
this._history ??= new TaskHistory(this)
@@ -1590,8 +1628,49 @@ export abstract class ComfyPage {
}
}
// Re-export shared constants and fixture
export { testComfySnapToGridGridSize } from './constants'
export const testComfySnapToGridGridSize = 50
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
comfyPage: async ({ page, request }, use, testInfo) => {
const comfyPage = new ComfyPage(page, request)
const { parallelIndex } = testInfo
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize
})
} catch (e) {
console.error(e)
}
await comfyPage.setup()
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})
const makeMatcher = function <T>(
getValue: (node: NodeReference) => Promise<T> | T,

View File

@@ -1,48 +0,0 @@
import { test as base } from '@playwright/test'
import { CloudComfyPage } from './CloudComfyPage'
import { ComfyMouse } from './ComfyMouse'
import type { ComfyPage } from './ComfyPage'
/**
* Cloud-specific fixture for ComfyPage.
* Uses Firebase auth persisted from globalSetupCloud.ts.
*/
export const comfyPageCloudFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
// Use the storageState saved by globalSetupCloud
storageState: 'browser_tests/.auth/cloudUser.json',
comfyPage: async ({ page, request }, use) => {
const comfyPage = new CloudComfyPage(page, request)
// Note: No setupUser needed - Firebase auth persisted via storageState
// Setup cloud-specific settings (optional - can customize per test)
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true
})
} catch (e) {
console.error('Failed to setup cloud settings:', e)
}
// Don't mock releases for cloud - cloud handles its own releases
await comfyPage.setup({ mockReleases: false })
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})

View File

@@ -1,78 +0,0 @@
import type { APIRequestContext, Page } from '@playwright/test'
import { ComfyPage } from './ComfyPage'
import type { FolderStructure } from './ComfyPage'
/**
* Localhost-specific implementation of ComfyPage.
* Uses devtools API and multi-user mode for test isolation.
*/
export class LocalhostComfyPage extends ComfyPage {
constructor(
page: Page,
request: APIRequestContext,
parallelIndex: number = 0
) {
super(page, request, parallelIndex)
}
async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.id}/workflows`
}
}
)
if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}
async setupUser(username: string): Promise<string | null> {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)
const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]
return id ? id : await this.createUser(username)
}
private async createUser(username: string): Promise<string> {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})
if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)
return await resp.json()
}
async setupSettings(settings: Record<string, any>): Promise<void> {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
{
data: settings
}
)
if (resp.status() !== 200) {
throw new Error(`Failed to setup settings: ${await resp.text()}`)
}
}
}

View File

@@ -1,56 +0,0 @@
import { test as base } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import type { ComfyPage } from './ComfyPage'
import { ComfyMouse } from './ComfyMouse'
import { testComfySnapToGridGridSize } from './constants'
import { LocalhostComfyPage } from './LocalhostComfyPage'
/**
* Localhost fixture for ComfyPage.
* Creates a test user and sets up default settings for stable testing.
*/
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
comfyPage: async ({ page, request }, use, testInfo) => {
const { parallelIndex } = testInfo
const comfyPage = new LocalhostComfyPage(page, request, parallelIndex)
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
if (userId) {
comfyPage.userIds[parallelIndex] = userId
}
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false
})
} catch (e) {
console.error(e)
}
await comfyPage.setup()
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})

View File

@@ -1,4 +0,0 @@
/**
* Shared constants for browser tests
*/
export const testComfySnapToGridGridSize = 50

View File

@@ -503,7 +503,7 @@ export class NodeReference {
for (const position of clickPositions) {
// Clear any selection first
await this.comfyPage.canvas.click({
position: { x: 250, y: 250 },
position: { x: 50, y: 50 },
force: true
})
await this.comfyPage.nextFrame()

View File

@@ -1,63 +0,0 @@
import { chromium } from '@playwright/test'
import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
import * as path from 'path'
dotenv.config()
/**
* Global setup for cloud tests.
* Authenticates with Firebase and saves auth state for test reuse.
*/
export default async function globalSetupCloud(config: FullConfig) {
const CLOUD_TEST_EMAIL = process.env.CLOUD_TEST_EMAIL
const CLOUD_TEST_PASSWORD = process.env.CLOUD_TEST_PASSWORD
if (!CLOUD_TEST_EMAIL || !CLOUD_TEST_PASSWORD) {
throw new Error(
'CLOUD_TEST_EMAIL and CLOUD_TEST_PASSWORD must be set in environment variables'
)
}
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
try {
// Navigate to cloud login page
await page.goto('https://stagingcloud.comfy.org/cloud/login', {
waitUntil: 'networkidle',
timeout: 30000
})
// Fill in email and password
await page.fill('#cloud-sign-in-email', CLOUD_TEST_EMAIL)
await page.fill('#cloud-sign-in-password', CLOUD_TEST_PASSWORD)
// Click login button
await page.click('button[type="submit"]')
// Wait for redirect to main app
await page.waitForURL('**/cloud', { timeout: 30000 })
// Wait a bit for auth tokens to be written to localStorage
await page.waitForTimeout(2000)
// Ensure .auth directory exists
const authDir = path.join(__dirname, '.auth')
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true })
}
// Save authentication state (includes localStorage with Firebase tokens)
await context.storageState({
path: 'browser_tests/.auth/cloudUser.json'
})
} catch (error) {
console.error('❌ Failed to authenticate:', error)
throw error
} finally {
await browser.close()
}
}

View File

@@ -1,36 +0,0 @@
# Cloud E2E Tests
## Setup
Cloud tests run against `https://stagingcloud.comfy.org` with Firebase authentication.
### Required GitHub Secrets
Add these to repository settings → Secrets → Actions:
- `CLOUD_TEST_EMAIL`: Firebase test account email
- `CLOUD_TEST_PASSWORD`: Firebase test account password
### Running Locally
```bash
# Set environment variables
export CLOUD_TEST_EMAIL="your-test-email@example.com"
export CLOUD_TEST_PASSWORD="your-password"
# Run cloud tests
pnpm exec playwright test --config=playwright.cloud.config.ts
```
### Running in CI
Workflow: `.github/workflows/ci-tests-e2e-cloud.yaml`
Trigger manually via Actions tab → "CI: Tests E2E Cloud" → Run workflow
### Test Structure
- Tests tagged with `@cloud` run only in cloud config
- Auth handled once in `globalSetupCloud.ts`
- Auth state saved to `browser_tests/.auth/cloudUser.json`
- Cloud fixture in `fixtures/ComfyPageCloud.ts`

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,49 +0,0 @@
/**
* @cloud
* Cloud E2E tests.
* Tests run against stagingcloud.comfy.org with authenticated user.
*/
import { expect } from '@playwright/test'
import { comfyPageCloudFixture as test } from '../fixtures/ComfyPageCloud'
test.describe('Cloud E2E @cloud', () => {
test('loads app with authentication', async ({ comfyPage }) => {
// App should be loaded from setup()
await expect(comfyPage.canvas).toBeVisible()
// Verify we're authenticated (cloud-specific check)
const isAuthenticated = await comfyPage.page.evaluate(() => {
// Check for Firebase auth in localStorage
const keys = Object.keys(localStorage)
return keys.some(
(key) => key.startsWith('firebase:') || key.includes('authUser')
)
})
expect(isAuthenticated).toBe(true)
})
test('can interact with canvas', async ({ comfyPage }) => {
// Basic canvas interaction
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toBeVisible()
// Close search box
await comfyPage.page.keyboard.press('Escape')
await expect(comfyPage.searchBox.input).not.toBeVisible()
})
test('can access settings dialog', async ({ comfyPage }) => {
// Open settings dialog
await comfyPage.page.click('button[data-testid="settings-button"]', {
timeout: 10000
})
// Settings dialog should be visible
await expect(comfyPage.page.locator('.p-dialog')).toBeVisible()
// Close settings
await comfyPage.closeDialog()
})
})

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function verifyCustomIconSvg(iconElement: Locator) {
const svgVariable = await iconElement.evaluate((element) => {

View File

@@ -2,7 +2,7 @@ import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -2,9 +2,11 @@ import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import {
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { testComfySnapToGridGridSize } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import type { ComfyApp } from '../../src/scripts/app'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
// TODO: there might be a better solution for this
// Helper function to pan canvas and select node

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
test.describe('Reroute Node', () => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

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

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture } from '../fixtures/comfyPageFixture'
import { comfyPageFixture } from '../fixtures/ComfyPage'
const test = comfyPageFixture

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants
const INITIAL_NAME = 'initial_slot_name'

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants
const RENAMED_INPUT_NAME = 'renamed_input'

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function checkTemplateFileExists(
page: Page,

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

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