diff --git a/.claude/commands/comprehensive-pr-review.md b/.claude/commands/comprehensive-pr-review.md index 84708564e..1b4047e78 100644 --- a/.claude/commands/comprehensive-pr-review.md +++ b/.claude/commands/comprehensive-pr-review.md @@ -67,9 +67,9 @@ This is critical for better file inspection: Use git locally for much faster analysis: -1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt` -2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt` -3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt` +1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt` +2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt` +3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt` ### Step 1.5: Create Analysis Cache diff --git a/.claude/commands/create-frontend-release.md b/.claude/commands/create-frontend-release.md index f1aa3e9f1..de6695710 100644 --- a/.claude/commands/create-frontend-release.md +++ b/.claude/commands/create-frontend-release.md @@ -128,7 +128,25 @@ echo "Last stable release: $LAST_STABLE" ### Step 4: Analyze Dependency Updates -1. **Check significant dependency updates:** +1. **Use pnpm's built-in dependency analysis:** + ```bash + # Get outdated dependencies with pnpm + pnpm outdated --format table > outdated-deps-${NEW_VERSION}.txt + + # Check for license compliance + pnpm licenses ls --json > licenses-${NEW_VERSION}.json + + # Analyze why specific dependencies exist + echo "Dependency analysis:" > dep-analysis-${NEW_VERSION}.md + MAJOR_DEPS=("vue" "vite" "@vitejs/plugin-vue" "typescript" "pinia") + for dep in "${MAJOR_DEPS[@]}"; do + echo -e "\n## $dep\n\`\`\`" >> dep-analysis-${NEW_VERSION}.md + pnpm why "$dep" >> dep-analysis-${NEW_VERSION}.md || echo "Not found" >> dep-analysis-${NEW_VERSION}.md + echo "\`\`\`" >> dep-analysis-${NEW_VERSION}.md + done + ``` + +2. **Check for significant dependency updates:** ```bash # Extract all dependency changes for major version bumps OTHER_DEP_CHANGES="" @@ -200,22 +218,48 @@ echo "Last stable release: $LAST_STABLE" PR data: [contents of prs-${NEW_VERSION}.json] ``` -3. **Generate GTM notification:** +3. **Generate GTM notification using this EXACT Slack-compatible format:** ```bash - # Save to gtm-summary-${NEW_VERSION}.md based on analysis - # If GTM-worthy features exist, include them with testing instructions - # If not, note that this is a maintenance/bug fix release - - # Check if notification is needed - if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then - echo "✅ No GTM notification needed for this release" - echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md" - else + # Only create file if GTM-worthy features exist: + if [ "$GTM_FEATURES_FOUND" = "true" ]; then + cat > gtm-summary-${NEW_VERSION}.md << 'EOF' + *GTM Summary: ComfyUI Frontend v${NEW_VERSION}* + + _Disclaimer: the below is AI-generated_ + + 1. *[Feature Title]* (#[PR_NUMBER]) + * *Author:* @[username] + * *Demo:* [Media Link or "No demo available"] + * *Why users should care:* [One compelling sentence] + * *Key Features:* + * [Feature detail 1] + * [Feature detail 2] + + 2. *[Feature Title]* (#[PR_NUMBER]) + * *Author:* @[username] + * *Demo:* [Media Link] + * *Why users should care:* [One compelling sentence] + * *Key Features:* + * [Feature detail 1] + * [Feature detail 2] + EOF echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md" echo "📤 Share this file in #gtm channel to notify the team" + else + echo "✅ No GTM notification needed for this release" + echo "📄 No gtm-summary file created - no marketing-worthy features" fi ``` + **CRITICAL Formatting Requirements:** + - Use single asterisk (*) for emphasis, NOT double (**) + - Use underscore (_) for italics + - Use 4 spaces for indentation (not tabs) + - Convert author names to @username format (e.g., "John Smith" → "@john") + - No section headers (#), no code language specifications + - Always include "Disclaimer: the below is AI-generated" + - Keep content minimal - no testing instructions, additional sections, etc. + ### Step 6: Version Preview **Version Preview:** @@ -228,17 +272,22 @@ echo "Last stable release: $LAST_STABLE" ### Step 7: Security and Dependency Audit -1. Run security audit: +1. Run pnpm security audit: ```bash - npm audit --audit-level moderate + pnpm audit --audit-level moderate + pnpm licenses ls --summary ``` 2. Check for known vulnerabilities in dependencies -3. Scan for hardcoded secrets or credentials: +3. Run comprehensive dependency health check: + ```bash + pnpm doctor + ``` +4. Scan for hardcoded secrets or credentials: ```bash git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found" ``` -4. Verify no sensitive data in recent commits -5. **SECURITY REVIEW**: Address any critical findings before proceeding? +5. Verify no sensitive data in recent commits +6. **SECURITY REVIEW**: Address any critical findings before proceeding? ### Step 8: Pre-Release Testing diff --git a/.claude/commands/create-hotfix-release.md b/.claude/commands/create-hotfix-release.md index de314309d..f35a8ad23 100644 --- a/.claude/commands/create-hotfix-release.md +++ b/.claude/commands/create-hotfix-release.md @@ -1,30 +1,85 @@ # Create Hotfix Release -This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step. +This command creates patch/hotfix releases for ComfyUI Frontend by backporting fixes to stable core branches. It handles both automated backports (preferred) and manual cherry-picking (fallback). + +**Process Overview:** +1. **Check automated backports first** (via labels) +2. **Skip to version bump** if backports already merged +3. **Manual cherry-picking** if automation failed +4. **Create patch release** with version bump +5. **Publish GitHub release** (manually uncheck "latest") +6. **Update ComfyUI requirements.txt** via PR -Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS +Create a hotfix release by backporting commits/PRs from main to a core branch: $ARGUMENTS Expected format: Comma-separated list of commits or PR numbers Examples: -- `abc123,def456,ghi789` (commits) -- `#1234,#5678` (PRs) -- `abc123,#1234,def456` (mixed) +- `#1234,#5678` (PRs - preferred) +- `abc123,def456` (commit hashes) +- `#1234,abc123` (mixed) -If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs. +If no arguments provided, the command will guide you through identifying commits/PRs to backport. ## Prerequisites -Before starting, ensure: -- You have push access to the repository -- GitHub CLI (`gh`) is authenticated -- You're on a clean working tree -- You understand the commits/PRs you're cherry-picking +- Push access to repository +- GitHub CLI (`gh`) authenticated +- Clean working tree +- Understanding of what fixes need backporting ## Hotfix Release Process -### Step 1: Identify Target Core Branch +### Step 1: Try Automated Backports First + +**Check if automated backports were attempted:** + +1. **For each PR, check existing backport labels:** + ```bash + gh pr view #1234 --json labels | jq -r '.labels[].name' + ``` + +2. **If no backport labels exist, add them now:** + ```bash + # Add backport labels (this triggers automated backports) + gh pr edit #1234 --add-label "needs-backport" + gh pr edit #1234 --add-label "1.24" # Replace with target version + ``` + +3. **Check for existing backport PRs:** + ```bash + # Check for backport PRs created by automation + PR_NUMBER=${ARGUMENTS%%,*} # Extract first PR number from arguments + PR_NUMBER=${PR_NUMBER#\#} # Remove # prefix + gh pr list --search "backport-${PR_NUMBER}-to" --json number,title,state,baseRefName + ``` + +4. **Handle existing backport scenarios:** + + **Scenario A: Automated backports already merged** + ```bash + # Check if backport PRs were merged to core branches + gh pr list --search "backport-${PR_NUMBER}-to" --state merged + ``` + - If backport PRs are merged → Skip to Step 10 (Version Bump) + - **CONFIRMATION**: Automated backports completed, proceeding to version bump? + + **Scenario B: Automated backport PRs exist but not merged** + ```bash + # Show open backport PRs that need merging + gh pr list --search "backport-${PR_NUMBER}-to" --state open + ``` + - **ACTION REQUIRED**: Merge the existing backport PRs first + - Use: `gh pr merge [PR_NUMBER] --merge` for each backport PR + - After merging, return to this command and skip to Step 10 (Version Bump) + - **CONFIRMATION**: Have you merged all backport PRs? Ready to proceed to version bump? + + **Scenario C: No automated backports or they failed** + - Continue to Step 2 for manual cherry-picking + - **CONFIRMATION**: Proceeding with manual cherry-picking because automation failed? + +### Step 2: Identify Target Core Branch 1. Fetch the current ComfyUI requirements.txt from master branch: ```bash @@ -36,7 +91,7 @@ Before starting, ensure: 5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*` 6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch? -### Step 2: Parse and Validate Arguments +### Step 3: Parse and Validate Arguments 1. Parse the comma-separated list of commits/PRs 2. For each item: @@ -49,7 +104,7 @@ Before starting, ensure: - **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits? 4. Validate all commit hashes exist in the repository -### Step 3: Analyze Target Changes +### Step 4: Analyze Target Changes 1. For each commit/PR to cherry-pick: - Display commit hash, author, date @@ -60,7 +115,7 @@ Before starting, ensure: 2. Identify potential conflicts by checking changed files 3. **CONFIRMATION REQUIRED**: Proceed with these commits? -### Step 4: Create Hotfix Branch +### Step 5: Create Hotfix Branch 1. Checkout the core branch (e.g., `core/1.23`) 2. Pull latest changes: `git pull origin core/X.Y` @@ -69,7 +124,7 @@ Before starting, ensure: - Example: `hotfix/1.23.4-20241120` 5. **CONFIRMATION REQUIRED**: Created branch correctly? -### Step 5: Cherry-pick Changes +### Step 6: Cherry-pick Changes For each commit: 1. Attempt cherry-pick: `git cherry-pick ` @@ -83,7 +138,7 @@ For each commit: - Run validation: `pnpm typecheck && pnpm lint` 4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid? -### Step 6: Create PR to Core Branch +### Step 7: Create PR to Core Branch 1. Push the hotfix branch: `git push origin hotfix/-` 2. Create PR using gh CLI: @@ -100,7 +155,7 @@ For each commit: - Impact assessment 5. **CONFIRMATION REQUIRED**: PR created correctly? -### Step 7: Wait for Tests +### Step 8: Wait for Tests 1. Monitor PR checks: `gh pr checks` 2. Display test results as they complete @@ -111,7 +166,7 @@ For each commit: 4. Wait for all required checks to pass 5. **CONFIRMATION REQUIRED**: All tests passing? -### Step 8: Merge Hotfix PR +### Step 9: Merge Hotfix PR 1. Verify all checks have passed 2. Check for required approvals @@ -119,7 +174,7 @@ For each commit: 4. Delete the hotfix branch 5. **CONFIRMATION REQUIRED**: PR merged successfully? -### Step 9: Create Version Bump +### Step 10: Create Version Bump 1. Checkout the core branch: `git checkout core/X.Y` 2. Pull latest changes: `git pull origin core/X.Y` @@ -131,7 +186,7 @@ For each commit: 7. Commit: `git commit -m "[release] Bump version to 1.23.5"` 8. **CONFIRMATION REQUIRED**: Version bump correct? -### Step 10: Create Release PR +### Step 11: Create Release PR 1. Push release branch: `git push origin release/1.23.5` 2. Create PR with Release label: @@ -184,7 +239,7 @@ For each commit: ``` 5. **CONFIRMATION REQUIRED**: Release PR has "Release" label? -### Step 11: Monitor Release Process +### Step 12: Monitor Release Process 1. Wait for PR checks to pass 2. **FINAL CONFIRMATION**: Ready to trigger release by merging? @@ -199,7 +254,102 @@ For each commit: - PyPI upload - pnpm types publication -### Step 12: Post-Release Verification +### Step 13: Manually Publish Draft Release + +**CRITICAL**: The release workflow creates a DRAFT release. You must manually publish it: + +1. **Go to GitHub Releases:** https://github.com/Comfy-Org/ComfyUI_frontend/releases +2. **Find the DRAFT release** (e.g., "v1.23.5 Draft") +3. **Click "Edit release"** +4. **UNCHECK "Set as the latest release"** âš ī¸ **CRITICAL** + - This prevents the hotfix from showing as "latest" + - Main branch should always be "latest release" +5. **Click "Publish release"** +6. **CONFIRMATION REQUIRED**: Draft release published with "latest" unchecked? + +### Step 14: Create ComfyUI Requirements.txt Update PR + +**IMPORTANT**: Create PR to update ComfyUI's requirements.txt via fork: + +1. **Setup fork (if needed):** + ```bash + # Check if fork already exists + if gh repo view ComfyUI --json owner | jq -r '.owner.login' | grep -q "$(gh api user --jq .login)"; then + echo "Fork already exists" + else + # Fork the ComfyUI repository + gh repo fork comfyanonymous/ComfyUI --clone=false + echo "Created fork of ComfyUI" + fi + ``` + +2. **Clone fork and create branch:** + ```bash + # Clone your fork (or use existing clone) + GITHUB_USER=$(gh api user --jq .login) + if [ ! -d "ComfyUI-fork" ]; then + gh repo clone ${GITHUB_USER}/ComfyUI ComfyUI-fork + fi + + cd ComfyUI-fork + git checkout master + git pull origin master + + # Create update branch + BRANCH_NAME="update-frontend-${NEW_VERSION}" + git checkout -b ${BRANCH_NAME} + ``` + +3. **Update requirements.txt:** + ```bash + # Update the version in requirements.txt + sed -i "s/comfyui-frontend-package==[0-9].*$/comfyui-frontend-package==${NEW_VERSION}/" requirements.txt + + # Verify the change + grep "comfyui-frontend-package" requirements.txt + + # Commit the change + git add requirements.txt + git commit -m "Bump frontend to ${NEW_VERSION}" + git push origin ${BRANCH_NAME} + ``` + +4. **Create PR from fork:** + ```bash + # Create PR using gh CLI from fork + gh pr create \ + --repo comfyanonymous/ComfyUI \ + --title "Bump frontend to ${NEW_VERSION}" \ + --body "$(cat <= 24) +node --version + +# Check if we're in a git repository +git status +``` + +## Step 1: Install pnpm + +```bash +# Check if pnpm is already installed +pnpm --version 2>/dev/null || { + echo "Installing pnpm..." + npm install -g pnpm +} + +# Verify pnpm installation +pnpm --version +``` + +## Step 2: Install Dependencies + +```bash +# Install all dependencies using pnpm +echo "Installing project dependencies..." +pnpm install + +# Verify node_modules exists and has packages +ls -la node_modules | head -5 +``` + +## Step 3: Verify Build + +```bash +# Run TypeScript type checking +echo "Running TypeScript checks..." +pnpm typecheck + +# Build the project +echo "Building project..." +pnpm build + +# Verify dist folder was created +ls -la dist/ +``` + +## Step 4: Run Unit Tests + +```bash +# Run unit tests +echo "Running unit tests..." +pnpm test:unit + +# If tests fail, show the output and stop +if [ $? -ne 0 ]; then + echo "❌ Unit tests failed. Please fix failing tests before continuing." + exit 1 +fi + +echo "✅ Unit tests passed successfully" +``` + +## Step 5: Verify Development Server + +```bash +# Start development server in background +echo "Starting development server..." +pnpm dev & +SERVER_PID=$! + +# Wait for server to start (check for port 5173 or similar) +echo "Waiting for server to start..." +sleep 10 + +# Check if server is running +if curl -s http://localhost:5173 > /dev/null 2>&1; then + echo "✅ Development server started successfully at http://localhost:5173" + + # Kill the background server + kill $SERVER_PID + wait $SERVER_PID 2>/dev/null +else + echo "❌ Development server failed to start or is not accessible" + kill $SERVER_PID 2>/dev/null + wait $SERVER_PID 2>/dev/null + exit 1 +fi +``` + +## Step 6: Final Verification + +```bash +# Run linting to ensure code quality +echo "Running linter..." +pnpm lint + +# Show project status +echo "" +echo "🎉 Repository setup complete!" +echo "" +echo "Available commands:" +echo " pnpm dev - Start development server" +echo " pnpm build - Build for production" +echo " pnpm test:unit - Run unit tests" +echo " pnpm test:component - Run component tests" +echo " pnpm typecheck - Run TypeScript checks" +echo " pnpm lint - Run ESLint" +echo " pnpm format - Format code with Prettier" +echo "" +echo "Next steps:" +echo "1. Run 'pnpm dev' to start developing" +echo "2. Open http://localhost:5173 in your browser" +echo "3. Check README.md for additional setup instructions" +``` + +## Troubleshooting + +If any step fails: + +1. **pnpm installation fails**: Try using `curl -fsSL https://get.pnpm.io/install.sh | sh -` +2. **Dependencies fail to install**: Try clearing cache with `pnpm store prune` and retry +3. **Build fails**: Check for TypeScript errors and fix them first +4. **Tests fail**: Review test output and fix failing tests +5. **Dev server fails**: Check if port 5173 is already in use + +## Manual Verification Steps + +After running the setup, manually verify: + +1. **Dependencies installed**: `ls node_modules | wc -l` should show many packages +2. **Build artifacts**: `ls dist/` should show built files +3. **Server accessible**: Open http://localhost:5173 in browser +4. **Hot reload works**: Edit a file and see changes reflect + +## Environment Requirements + +- Node.js >= 24 +- Git repository +- Internet connection for package downloads +- Available ports (typically 5173 for dev server) \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index fa318da4f..06139b08a 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -4,3 +4,24 @@ # npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files) c53f197de2a3e0fa66b16dedc65c131235c1c4b6 + +# Reorganize renderer components into domain-driven folder structure +c8a83a9caede7bdb5f8598c5492b07d08c339d49 + +# Domain-driven design (DDD) refactors - September 2025 +# These commits reorganized the codebase into domain-driven architecture + +# [refactor] Improve renderer domain organization (#5552) +6349ceee6c0a57fc7992e85635def9b6e22eaeb2 + +# [refactor] Improve settings domain organization (#5550) +4c8c4a1ad4f53354f700a33ea1b95262aeda2719 + +# [refactor] Improve workflow domain organization (#5584) +ca312fd1eab540cc4ddc0e3d244d38b3858574f0 + +# [refactor] Move thumbnail functionality to renderer/core domain (#5586) +e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa + +# [refactor] Improve updates/notifications domain organization (#5590) +27ab355f9c73415dc39f4d3f512b02308f847801 diff --git a/.gitattributes b/.gitattributes index 749554ee1..bd0518cde 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,7 +9,8 @@ *.mts text eol=lf *.ts text eol=lf *.vue text eol=lf +*.yaml text eol=lf # Generated files src/types/comfyRegistryTypes.ts linguist-generated=true -src/types/generatedManagerTypes.ts linguist-generated=true +src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index f9106caee..178bd4ee8 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -2,12 +2,27 @@ name: Auto Backport on: pull_request_target: - types: [closed] + types: [closed, labeled] branches: [main] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to backport' + required: true + type: string + force_rerun: + description: 'Force rerun even if backports exist' + required: false + type: boolean + default: false jobs: backport: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport') + if: > + (github.event_name == 'pull_request_target' && + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'needs-backport')) || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write @@ -15,6 +30,35 @@ jobs: issues: write steps: + - name: Validate inputs for manual triggers + if: github.event_name == 'workflow_dispatch' + run: | + # Validate PR number format + if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid PR number format. Must be a positive integer." + exit 1 + fi + + # Validate PR exists and is merged + if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then + echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible." + exit 1 + fi + + MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged') + if [ "$MERGED" != "true" ]; then + echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported." + exit 1 + fi + + # Validate PR has needs-backport label + if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then + echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label." + exit 1 + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository uses: actions/checkout@v4 with: @@ -25,13 +69,49 @@ 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: Extract version labels + if: steps.check-existing.outputs.skip != 'true' id: versions run: | # Extract version labels (e.g., "1.24", "1.22") VERSIONS="" - LABELS='${{ toJSON(github.event.pull_request.labels) }}' - for label in $(echo "$LABELS" | jq -r '.[].name'); do + + 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 + + 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 @@ -52,14 +132,23 @@ jobs: echo "Found version labels: ${VERSIONS}" - name: Backport commits + if: steps.check-existing.outputs.skip != 'true' id: backport env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | FAILED="" SUCCESS="" + + # Get PR data for manual triggers + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + else + PR_TITLE="${{ github.event.pull_request.title }}" + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + fi for version in ${{ steps.versions.outputs.versions }}; do echo "::group::Backporting to core/${version}" @@ -109,14 +198,21 @@ jobs: fi - name: Create PR for each successful backport - if: 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 }} run: | - PR_TITLE="${{ github.event.pull_request.title }}" - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - + # Get PR data for manual triggers + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') + else + PR_TITLE="${{ github.event.pull_request.title }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + fi + for backport in ${{ steps.backport.outputs.success }}; do IFS=':' read -r version branch <<< "${backport}" @@ -141,13 +237,20 @@ jobs: done - name: Comment on failures - if: failure() && steps.backport.outputs.failed + if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed env: GH_TOKEN: ${{ github.token }} run: | - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit) + PR_NUMBER="${{ inputs.pr_number }}" + PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') + MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + else + PR_NUMBER="${{ github.event.pull_request.number }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + fi for failure in ${{ steps.backport.outputs.failed }}; do IFS=':' read -r version reason conflicts <<< "${failure}" diff --git a/.github/workflows/chromatic.yaml b/.github/workflows/chromatic.yaml index ed2314e80..127186d68 100644 --- a/.github/workflows/chromatic.yaml +++ b/.github/workflows/chromatic.yaml @@ -12,9 +12,6 @@ jobs: runs-on: ubuntu-latest # Only run for PRs from version-bump-* branches or manual triggers if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-') - permissions: - pull-requests: write - issues: write steps: - name: Checkout code uses: actions/checkout@v4 @@ -32,29 +29,6 @@ jobs: node-version: '20' cache: 'pnpm' - - name: Get current time - id: current-time - run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT - - - name: Comment PR - Build Started - if: github.event_name == 'pull_request' - continue-on-error: true - uses: edumserrano/find-create-or-update-comment@v3 - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: append - body: | - - ## 🎨 Storybook Build Status - - 🔄 **Building Storybook and running visual tests...** - - âŗ Build started at: ${{ steps.current-time.outputs.time }} UTC - - --- - *This comment will be updated when the build completes* - name: Cache tool outputs uses: actions/cache@v4 @@ -81,37 +55,3 @@ jobs: autoAcceptChanges: 'main' # Auto-accept changes on main branch exitOnceUploaded: true # Don't wait for UI tests to complete - - name: Get completion time - id: completion-time - if: always() - run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT - - - name: Comment PR - Build Complete - if: github.event_name == 'pull_request' && always() - continue-on-error: true - uses: edumserrano/find-create-or-update-comment@v3 - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: replace - body: | - - ## 🎨 Storybook Build Status - - ${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}** - - ⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC - - ### 📊 Build Summary - - **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }} - - **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }} - - **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }} - - **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }} - - ### 🔗 Links - ${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }} - ${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }} - - --- - ${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || 'âš ī¸ Please check the workflow logs for error details.' }} diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 3ec61cb3c..d1eecafe3 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -47,6 +47,7 @@ jobs: needs: wait-for-ci if: needs.wait-for-ci.outputs.should-proceed == 'true' runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -69,19 +70,17 @@ jobs: pnpm install -g typescript @vue/compiler-sfc - name: Run Claude PR Review - uses: anthropics/claude-code-action@main + uses: anthropics/claude-code-action@v1.0.6 with: label_trigger: "claude-review" - direct_prompt: | + 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. Each issue must be posted as a separate inline comment on the specific line of code. anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - max_turns: 256 - timeout_minutes: 30 - allowed_tools: "Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch" + 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'" env: PR_NUMBER: ${{ github.event.pull_request.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/create-release-candidate-branch.yaml b/.github/workflows/create-release-candidate-branch.yaml index 84b545478..e3fcd9e2b 100644 --- a/.github/workflows/create-release-candidate-branch.yaml +++ b/.github/workflows/create-release-candidate-branch.yaml @@ -128,45 +128,6 @@ jobs: echo "- Critical security patches" echo "- Documentation updates" - - name: Create branch protection rules - if: steps.check_version.outputs.is_minor_bump == 'true' && env.branch_exists != 'true' - env: - GITHUB_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}" - - # Create branch protection using GitHub API - echo "Setting up branch protection for $BRANCH_NAME..." - - RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - "https://api.github.com/repos/${{ github.repository }}/branches/$BRANCH_NAME/protection" \ - -d '{ - "required_status_checks": { - "strict": true, - "contexts": ["lint-and-format", "test", "playwright-tests"] - }, - "enforce_admins": false, - "required_pull_request_reviews": { - "required_approving_review_count": 1, - "dismiss_stale_reviews": true - }, - "restrictions": null, - "allow_force_pushes": false, - "allow_deletions": false - }') - - HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [[ "$HTTP_CODE" -eq 200 ]] || [[ "$HTTP_CODE" -eq 201 ]]; then - echo "✅ Branch protection successfully applied" - else - echo "âš ī¸ Failed to apply branch protection (HTTP $HTTP_CODE)" - echo "Response: $BODY" - # Don't fail the workflow, just warn - fi - name: Post summary if: steps.check_version.outputs.is_minor_bump == 'true' diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index a5617c196..959d01739 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -32,11 +32,10 @@ jobs: with: repository: Comfy-Org/ComfyUI_frontend path: ComfyUI_frontend - - name: Checkout ComfyUI_devtools - uses: actions/checkout@v4 - with: - repository: Comfy-Org/ComfyUI_devtools - path: ComfyUI/custom_nodes/ComfyUI_devtools + - name: 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@v4 with: @@ -79,7 +78,7 @@ jobs: wait-for-it --service 127.0.0.1:8188 -t 600 working-directory: ComfyUI - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Start dev server # Run electron dev server as it is a superset of the web dev server @@ -87,7 +86,7 @@ jobs: run: pnpm dev:electron & working-directory: ComfyUI_frontend - name: Capture base i18n - run: npx tsx scripts/diff-i18n capture + run: pnpm exec tsx scripts/diff-i18n capture working-directory: ComfyUI_frontend - name: Update en.json run: pnpm collect-i18n @@ -100,7 +99,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} working-directory: ComfyUI_frontend - name: Diff base vs updated i18n - run: npx tsx scripts/diff-i18n diff + run: pnpm exec tsx scripts/diff-i18n diff working-directory: ComfyUI_frontend - name: Update i18n in custom node repository run: | diff --git a/.github/workflows/i18n-node-defs.yaml b/.github/workflows/i18n-node-defs.yaml index 1327db3cf..d9105a4ac 100644 --- a/.github/workflows/i18n-node-defs.yaml +++ b/.github/workflows/i18n-node-defs.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3 - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Start dev server # Run electron dev server as it is a superset of the web dev server diff --git a/.github/workflows/i18n.yaml b/.github/workflows/i18n.yaml index 2f332e9dc..566a335b5 100644 --- a/.github/workflows/i18n.yaml +++ b/.github/workflows/i18n.yaml @@ -25,8 +25,15 @@ jobs: key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }} restore-keys: | i18n-tools-cache-${{ runner.os }}- + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }} + restore-keys: | + playwright-browsers-${{ runner.os }}- - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Start dev server # Run electron dev server as it is a superset of the web dev server @@ -34,7 +41,7 @@ jobs: run: pnpm dev:electron & working-directory: ComfyUI_frontend - name: Update en.json - run: pnpm collect-i18n -- scripts/collect-i18n-general.ts + run: pnpm collect-i18n env: PLAYWRIGHT_TEST_URL: http://localhost:5173 working-directory: ComfyUI_frontend diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/lint-and-format.yaml index b715328db..3b6bf1538 100644 --- a/.github/workflows/lint-and-format.yaml +++ b/.github/workflows/lint-and-format.yaml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 - name: Install pnpm diff --git a/.github/workflows/pr-playwright-deploy.yaml b/.github/workflows/pr-playwright-deploy.yaml new file mode 100644 index 000000000..19bb28253 --- /dev/null +++ b/.github/workflows/pr-playwright-deploy.yaml @@ -0,0 +1,92 @@ +name: PR Playwright Deploy (Forks) + +on: + workflow_run: + workflows: ["Tests CI"] + types: [requested, completed] + +env: + DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p' + +jobs: + deploy-and-comment-forked-pr: + runs-on: ubuntu-latest + if: | + github.repository == 'Comfy-Org/ComfyUI_frontend' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.head_repository != null && + github.event.workflow_run.repository != null && + github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name + permissions: + pull-requests: write + actions: read + steps: + - name: Log workflow trigger info + run: | + echo "Repository: ${{ github.repository }}" + echo "Event: ${{ github.event.workflow_run.event }}" + echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}" + echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}" + echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get PR Number + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + }); + + const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha); + + if (!pr) { + console.log('No PR found for SHA:', context.payload.workflow_run.head_sha); + return null; + } + + console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`); + return pr.number; + + - name: Handle Test Start + if: steps.pr.outputs.result != 'null' && github.event.action == 'requested' + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh + ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ + "${{ steps.pr.outputs.result }}" \ + "${{ github.event.workflow_run.head_branch }}" \ + "starting" \ + "$(date -u '${{ env.DATE_FORMAT }}')" + + - name: Download and Deploy Reports + if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + pattern: playwright-report-* + path: reports + + - name: Handle Test Completion + if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + GITHUB_TOKEN: ${{ github.token }} + run: | + # Rename merged report if exists + [ -d "reports/playwright-report-chromium-merged" ] && \ + mv reports/playwright-report-chromium-merged reports/playwright-report-chromium + + chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh + ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ + "${{ steps.pr.outputs.result }}" \ + "${{ github.event.workflow_run.head_branch }}" \ + "completed" \ No newline at end of file diff --git a/.github/workflows/pr-storybook-comment.yaml b/.github/workflows/pr-storybook-comment.yaml new file mode 100644 index 000000000..53691d826 --- /dev/null +++ b/.github/workflows/pr-storybook-comment.yaml @@ -0,0 +1,126 @@ +name: PR Storybook Comment + +on: + workflow_run: + workflows: ['Chromatic'] + types: [requested, completed] + +jobs: + comment-storybook: + runs-on: ubuntu-latest + if: >- + github.repository == 'Comfy-Org/ComfyUI_frontend' + && github.event.workflow_run.event == 'pull_request' + && startsWith(github.event.workflow_run.head_branch, 'version-bump-') + permissions: + pull-requests: write + actions: read + steps: + - name: Get PR number + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pullRequests } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`, + }); + + if (pullRequests.length === 0) { + console.log('No open PR found for this branch'); + return null; + } + + return pullRequests[0].number; + + - name: Log when no PR found + if: steps.pr.outputs.result == 'null' + run: | + echo "âš ī¸ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}" + echo "Workflow run ID: ${{ github.event.workflow_run.id }}" + echo "Repository: ${{ github.event.workflow_run.repository.full_name }}" + echo "Event: ${{ github.event.workflow_run.event }}" + + - name: Get workflow run details + if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + id: workflow-run + uses: actions/github-script@v7 + with: + script: | + const run = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + return { + conclusion: run.data.conclusion, + html_url: run.data.html_url + }; + + - name: Get completion time + id: completion-time + run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT + + - name: Comment PR - Storybook Started + if: steps.pr.outputs.result != 'null' && github.event.action == 'requested' + uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0 + with: + issue-number: ${{ steps.pr.outputs.result }} + body-includes: '' + comment-author: 'github-actions[bot]' + edit-mode: replace + body: | + + ## 🎨 Storybook Build Status + + comfy-loading-gif **Build is starting...** + + ⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC + + ### 🚀 Building Storybook + - đŸ“Ļ Installing dependencies... + - 🔧 Building Storybook components... + - 🎨 Running Chromatic visual tests... + + --- + âąī¸ Please wait while the Storybook build is in progress... + + - name: Comment PR - Storybook Complete + if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0 + with: + issue-number: ${{ steps.pr.outputs.result }} + body-includes: '' + comment-author: 'github-actions[bot]' + edit-mode: replace + body: | + + ## 🎨 Storybook Build Status + + ${{ + fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅' + || fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'â­ī¸' + || fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'đŸšĢ' + || '❌' + }} **${{ + fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!' + || fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.' + || fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.' + || 'Build failed!' + }}** + + ⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC + + ### 🔗 Links + - [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }}) + + --- + ${{ + fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!' + || fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'â„šī¸ Chromatic was skipped for this PR.' + || fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'â„šī¸ The Chromatic run was cancelled.' + || 'âš ī¸ Please check the workflow logs for error details.' + }} diff --git a/.github/workflows/publish-frontend-types.yaml b/.github/workflows/publish-frontend-types.yaml new file mode 100644 index 000000000..142a22a93 --- /dev/null +++ b/.github/workflows/publish-frontend-types.yaml @@ -0,0 +1,139 @@ +name: Publish Frontend Types + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 1.26.7)' + required: true + type: string + dist_tag: + description: 'npm dist-tag to use' + required: true + default: latest + type: string + ref: + description: 'Git ref to checkout (commit SHA, tag, or branch)' + required: false + type: string + workflow_call: + inputs: + version: + required: true + type: string + dist_tag: + required: false + type: string + default: latest + ref: + required: false + type: string + +concurrency: + group: publish-frontend-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }} + cancel-in-progress: false + +jobs: + publish_types_manual: + name: Publish @comfyorg/comfyui-frontend-types + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Validate inputs + shell: bash + run: | + set -euo pipefail + VERSION="${{ inputs.version }}" + SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' + if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then + echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2 + exit 1 + fi + + - name: Determine ref to checkout + id: resolve_ref + shell: bash + run: | + set -euo pipefail + REF="${{ inputs.ref }}" + VERSION="${{ inputs.version }}" + if [ -n "$REF" ]; then + if ! git check-ref-format --allow-onelevel "$REF"; then + echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2 + exit 1 + fi + echo "ref=$REF" >> "$GITHUB_OUTPUT" + else + echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout repository + uses: actions/checkout@v5 + with: + ref: ${{ steps.resolve_ref.outputs.ref }} + fetch-depth: 1 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 'lts/*' + cache: 'pnpm' + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: pnpm install --frozen-lockfile + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + + - name: Build types + run: pnpm build:types + + - name: Verify version matches input + id: verify + shell: bash + run: | + PKG_VERSION=$(node -p "require('./package.json').version") + TYPES_PKG_VERSION=$(node -p "require('./dist/package.json').version") + if [ "$PKG_VERSION" != "${{ inputs.version }}" ]; then + echo "Error: package.json version $PKG_VERSION does not match input ${{ inputs.version }}" >&2 + exit 1 + fi + if [ "$TYPES_PKG_VERSION" != "${{ inputs.version }}" ]; then + echo "Error: dist/package.json version $TYPES_PKG_VERSION does not match input ${{ inputs.version }}" >&2 + exit 1 + fi + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + + - name: Check if version already on npm + id: check_npm + shell: bash + run: | + set -euo pipefail + NAME=$(node -p "require('./dist/package.json').name") + VER="${{ steps.verify.outputs.version }}" + STATUS=0 + OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$? + if [ "$STATUS" -eq 0 ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish." + else + if echo "$OUTPUT" | grep -q "E404"; then + echo "exists=false" >> "$GITHUB_OUTPUT" + else + echo "::error title=Registry lookup failed::$OUTPUT" >&2 + exit "$STATUS" + fi + fi + + - name: Publish package + if: steps.check_npm.outputs.exists == 'false' + run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks + working-directory: dist + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8958ce147..c359e3da4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4 with: @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download dist artifact uses: actions/download-artifact@v4 with: @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download dist artifact uses: actions/download-artifact@v4 with: @@ -126,34 +126,8 @@ jobs: publish_types: needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - registry-url: https://registry.npmjs.org - - - name: Cache tool outputs - uses: actions/cache@v4 - with: - path: | - .cache - tsconfig.tsbuildinfo - dist - key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - types-tools-cache-${{ runner.os }}- - - - run: pnpm install --frozen-lockfile - - run: pnpm build:types - - name: Publish package - run: pnpm publish --access public - working-directory: dist - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + uses: ./.github/workflows/publish-frontend-types.yaml + with: + version: ${{ needs.build.outputs.version }} + ref: ${{ github.event.pull_request.merge_commit_sha }} + secrets: inherit diff --git a/.github/workflows/test-browser-exp.yaml b/.github/workflows/test-browser-exp.yaml index f260c2a3d..e174e89c3 100644 --- a/.github/workflows/test-browser-exp.yaml +++ b/.github/workflows/test-browser-exp.yaml @@ -11,12 +11,19 @@ jobs: if: github.event.label.name == 'New Browser Test Expectations' steps: - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3 + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }} + restore-keys: | + playwright-browsers-${{ runner.os }}- - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Run Playwright tests and update snapshots id: playwright-tests - run: npx playwright test --update-snapshots + run: pnpm exec playwright test --update-snapshots continue-on-error: true working-directory: ComfyUI_frontend - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index 4c5e3d25c..640615d99 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -7,15 +7,12 @@ on: branches-ignore: [wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*] -env: - DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p' - jobs: setup: runs-on: ubuntu-latest outputs: cache-key: ${{ steps.cache-key.outputs.key }} - sanitized-branch: ${{ steps.branch-info.outputs.sanitized }} + playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }} steps: - name: Checkout ComfyUI uses: actions/checkout@v4 @@ -30,12 +27,10 @@ jobs: repository: 'Comfy-Org/ComfyUI_frontend' path: 'ComfyUI_frontend' - - name: Checkout ComfyUI_devtools - uses: actions/checkout@v4 - with: - repository: 'Comfy-Org/ComfyUI_devtools' - path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' + - 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 @@ -48,30 +43,6 @@ jobs: cache: 'pnpm' cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml' - - name: Get current time - id: current-time - run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT - - - name: Comment PR - Tests Started - if: github.event_name == 'pull_request' - continue-on-error: true - uses: edumserrano/find-create-or-update-comment@v3 - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: append - body: | - - - --- - - claude-loading-gif - [${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers... - - --- - *This comment will be updated when tests complete* - - name: Cache tool outputs uses: actions/cache@v4 with: @@ -94,13 +65,12 @@ jobs: id: cache-key run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT - - name: Generate sanitized branch name - id: branch-info - run: | - # Get branch name and sanitize it for Cloudflare branch names - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') - echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT + - name: Playwright Version + id: playwright-version + run: | + PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version') + echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT + working-directory: ComfyUI_frontend - name: Save cache uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 @@ -110,17 +80,17 @@ jobs: ComfyUI_frontend key: comfyui-setup-${{ steps.cache-key.outputs.key }} - playwright-tests: + # Sharded chromium tests + playwright-tests-chromium-sharded: needs: setup runs-on: ubuntu-latest permissions: - pull-requests: write - issues: write contents: read strategy: fail-fast: false matrix: - browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] steps: - name: Wait for cache propagation run: sleep 10 @@ -144,32 +114,85 @@ jobs: python-version: '3.10' cache: 'pip' - - name: Get current time - id: current-time - run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT - - - name: Set project name - id: project-name + - name: Install requirements run: | - if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then - echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT - else - echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT - fi - echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT + 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: Comment PR - Browser Test Started - if: github.event_name == 'pull_request' - continue-on-error: true - uses: edumserrano/find-create-or-update-comment@v3 + + - name: Cache Playwright Browsers + uses: actions/cache@v4 + id: cache-playwright-browsers with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: append - body: | - claude-loading-gif - ${{ matrix.browser }}: Running tests... + path: '~/.cache/ms-playwright' + key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}' + + - name: Install Playwright Browsers + if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' + 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' + run: pnpm exec playwright install-deps + working-directory: ComfyUI_frontend + + - 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 + + - 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 + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: blob-report-chromium-${{ matrix.shardIndex }} + path: blob-report/ + retention-days: 1 + + playwright-tests: + # Ideally, each shard runs test in 6 minutes, but allow up to 15 minutes + timeout-minutes: 15 + needs: setup + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + browser: [chromium-2x, chromium-0.5x, mobile-chrome] + steps: + - name: Wait for cache propagation + run: sleep 10 + + - name: Restore cached setup + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 + with: + fail-on-cache-miss: true + path: | + ComfyUI + ComfyUI_frontend + key: comfyui-setup-${{ needs.setup.outputs.cache-key }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + cache: 'pip' - name: Install requirements run: | @@ -179,215 +202,156 @@ jobs: pip install wait-for-it working-directory: ComfyUI + - name: Cache Playwright Browsers + uses: actions/cache@v4 + id: cache-playwright-browsers + with: + path: '~/.cache/ms-playwright' + key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}' + + - name: Install Playwright Browsers + if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' + 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' + run: pnpm exec playwright install-deps + working-directory: ComfyUI_frontend + - 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 - - name: Cache Playwright browsers - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ matrix.browser }} - restore-keys: | - playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}- - playwright-browsers-${{ runner.os }}- - - - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps - working-directory: ComfyUI_frontend - - - name: Install Wrangler - run: pnpm install -g wrangler - - name: Run Playwright tests (${{ matrix.browser }}) id: playwright - run: npx playwright test --project=${{ matrix.browser }} --reporter=html + run: | + # Run tests with both HTML and JSON reporters + PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ + pnpm exec playwright test --project=${{ matrix.browser }} \ + --reporter=list \ + --reporter=html \ + --reporter=json working-directory: ComfyUI_frontend - uses: actions/upload-artifact@v4 - if: always() # note: use always() to allow results to be upload/report even tests failed. + if: always() with: name: playwright-report-${{ matrix.browser }} path: ComfyUI_frontend/playwright-report/ retention-days: 30 - - name: Deploy to Cloudflare Pages (${{ matrix.browser }}) - id: cloudflare-deploy - if: always() - continue-on-error: true - run: | - # Retry logic for wrangler deploy (3 attempts) - RETRY_COUNT=0 - MAX_RETRIES=3 - SUCCESS=false - - while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do - RETRY_COUNT=$((RETRY_COUNT + 1)) - echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..." - - if npx wrangler pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then - SUCCESS=true - echo "Deployment successful on attempt $RETRY_COUNT" - else - echo "Deployment failed on attempt $RETRY_COUNT" - if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then - echo "Retrying in 10 seconds..." - sleep 10 - fi - fi - done - - if [ $SUCCESS = false ]; then - echo "All deployment attempts failed" - exit 1 - fi - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # Merge sharded test reports + merge-reports: + needs: [playwright-tests-chromium-sharded] + runs-on: ubuntu-latest + if: ${{ !cancelled() }} + steps: + - name: Checkout ComfyUI_frontend + uses: actions/checkout@v4 + with: + repository: 'Comfy-Org/ComfyUI_frontend' + path: 'ComfyUI_frontend' - - name: Save deployment info for summary - if: always() - run: | - mkdir -p deployment-info - # Use step conclusion to determine test result - if [ "${{ steps.playwright.conclusion }}" = "success" ]; then - EXIT_CODE="0" - else - EXIT_CODE="1" - fi - DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}" - echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 - - name: Upload deployment info - if: always() + - 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: ComfyUI_frontend/all-blob-reports + pattern: blob-report-chromium-* + merge-multiple: true + + - name: Merge into HTML Report + run: | + # Generate HTML report + pnpm exec playwright merge-reports --reporter=html ./all-blob-reports + # 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: deployment-info-${{ matrix.browser }} - path: deployment-info/ - retention-days: 1 + name: playwright-report-chromium + path: ComfyUI_frontend/playwright-report/ + retention-days: 30 - - name: Get completion time - id: completion-time - if: always() - run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT + #### BEGIN Deployment and commenting (non-forked PRs only) + # when using pull_request event, we have permission to comment directly + # if its a forked repo, we need to use workflow_run event in a separate workflow (pr-playwright-deploy.yaml) - - name: Comment PR - Browser Test Complete - if: always() && github.event_name == 'pull_request' - continue-on-error: true - uses: edumserrano/find-create-or-update-comment@v3 - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: append - body: | - ${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}) - - comment-summary: - needs: playwright-tests + # Post starting comment for non-forked PRs + comment-on-pr-start: runs-on: ubuntu-latest - if: always() && github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false permissions: pull-requests: write steps: - - name: Download all deployment info + - name: Checkout repository + uses: actions/checkout@v4 + + - 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 }} + run: | + chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh + ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ + "${{ github.event.pull_request.number }}" \ + "${{ 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] + runs-on: ubuntu-latest + if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + permissions: + pull-requests: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download all playwright reports uses: actions/download-artifact@v4 with: - pattern: deployment-info-* - merge-multiple: true - path: deployment-info - - - name: Get completion time - id: completion-time - run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT - - - name: Generate comment body - id: comment-body + 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: | - echo "" > comment.md - echo "## 🎭 Playwright Test Results" >> comment.md - echo "" >> comment.md - - # Check if all tests passed - ALL_PASSED=true - for file in deployment-info/*.txt; do - if [ -f "$file" ]; then - browser=$(basename "$file" .txt) - info=$(cat "$file") - exit_code=$(echo "$info" | cut -d'|' -f2) - if [ "$exit_code" != "0" ]; then - ALL_PASSED=false - break - fi - fi - done - - if [ "$ALL_PASSED" = "true" ]; then - echo "✅ **All tests passed across all browsers!**" >> comment.md - else - echo "❌ **Some tests failed!**" >> comment.md - fi - - echo "" >> comment.md - echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md - echo "" >> comment.md - echo "### 📊 Test Reports by Browser" >> comment.md - - for file in deployment-info/*.txt; do - if [ -f "$file" ]; then - browser=$(basename "$file" .txt) - info=$(cat "$file") - exit_code=$(echo "$info" | cut -d'|' -f2) - url=$(echo "$info" | cut -d'|' -f3) - - if [ "$exit_code" = "0" ]; then - status="✅" - else - status="❌" - fi - - echo "- $status **$browser**: [View Report]($url)" >> comment.md - fi - done - - echo "" >> comment.md - echo "---" >> comment.md - if [ "$ALL_PASSED" = "true" ]; then - echo "🎉 Your tests are passing across all browsers!" >> comment.md - else - echo "âš ī¸ Please check the test reports for details on failures." >> comment.md - fi - - - name: Comment PR - Tests Complete - continue-on-error: true - uses: edumserrano/find-create-or-update-comment@v3 - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: replace - body-path: comment.md - - - name: Check test results and fail if needed - run: | - # Check if all tests passed and fail the job if not - ALL_PASSED=true - for file in deployment-info/*.txt; do - if [ -f "$file" ]; then - info=$(cat "$file") - exit_code=$(echo "$info" | cut -d'|' -f2) - if [ "$exit_code" != "0" ]; then - ALL_PASSED=false - break - fi - fi - done - - if [ "$ALL_PASSED" = "false" ]; then - echo "❌ Tests failed in one or more browsers. Failing the CI job." - exit 1 - else - echo "✅ All tests passed across all browsers!" - fi + ./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) \ No newline at end of file diff --git a/.github/workflows/update-electron-types.yaml b/.github/workflows/update-electron-types.yaml index 04d8cc436..96f85f6b0 100644 --- a/.github/workflows/update-electron-types.yaml +++ b/.github/workflows/update-electron-types.yaml @@ -35,12 +35,12 @@ jobs: electron-types-tools-cache-${{ runner.os }}- - name: Update electron types - run: pnpm install @comfyorg/comfyui-electron-types@latest + run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest - name: Get new version id: get-version run: | - NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./pnpm-lock.yaml')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)") + NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].dependencies."@comfyorg/comfyui-electron-types".version') echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT - name: Create Pull Request diff --git a/.github/workflows/update-manager-types.yaml b/.github/workflows/update-manager-types.yaml index 8f3bf6cdb..de5b799da 100644 --- a/.github/workflows/update-manager-types.yaml +++ b/.github/workflows/update-manager-types.yaml @@ -68,7 +68,7 @@ jobs: - name: Generate Manager API types run: | echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..." - npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts + pnpm dlx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts - name: Validate generated types run: | @@ -121,4 +121,4 @@ jobs: labels: Manager delete-branch: true add-paths: | - src/types/generatedManagerTypes.ts + src/types/generatedManagerTypes.ts \ No newline at end of file diff --git a/.github/workflows/update-registry-types.yaml b/.github/workflows/update-registry-types.yaml index 0cd2c41da..4a84e9f43 100644 --- a/.github/workflows/update-registry-types.yaml +++ b/.github/workflows/update-registry-types.yaml @@ -68,7 +68,7 @@ jobs: - name: Generate API types run: | echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..." - npx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts + pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts - name: Validate generated types run: | diff --git a/.gitignore b/.gitignore index c2e17be87..e5bb5f107 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ dist-ssr *.local # Claude configuration .claude/*.local.json +.claude/*.local.md +.claude/*.local.txt +CLAUDE.local.md # Editor directories and files .vscode/* @@ -43,6 +46,7 @@ components.d.ts tests-ui/data/* tests-ui/ComfyUI_examples tests-ui/workflows/examples +coverage/ # Browser tests /test-results/ @@ -50,6 +54,7 @@ tests-ui/workflows/examples /blob-report/ /playwright/.cache/ browser_tests/**/*-win32.png +browser_tests/local/ .env @@ -76,3 +81,12 @@ vite.config.mts.timestamp-*.mjs *storybook.log storybook-static +# MCP Servers +.playwright-mcp/* + +.nx/cache +.nx/workspace-data +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md +vite.config.*.timestamp* +vitest.config.*.timestamp* diff --git a/.husky/pre-commit b/.husky/pre-commit index 6b8a399e4..c0b5cf437 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env bash -npx lint-staged -npx tsx scripts/check-unused-i18n-keys.ts \ No newline at end of file +pnpm exec lint-staged +pnpm exec tsx scripts/check-unused-i18n-keys.ts \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..ec1fc17d0 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +# Run Knip with cache via package script +pnpm knip + diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 0429c3578..2efe5f966 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -9,7 +9,7 @@ module.exports = defineConfig({ entry: 'src/locales/en', entryLocale: 'en', output: 'src/locales', - outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'], + outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'], reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 82e215be0..000000000 --- a/.mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "playwright": { - "command": "npx", - "args": ["-y", "@executeautomation/playwright-mcp-server"] - }, - "context7": { - "command": "npx", - "args": ["-y", "@upstash/context7-mcp"] - } - } -} \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ae90f7051 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-workspace-root-check=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..a45fd52cc --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.storybook/main.ts b/.storybook/main.ts index a799ec143..e8021974b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -15,26 +15,37 @@ const config: StorybookConfig = { async viteFinal(config) { // Use dynamic import to avoid CJS deprecation warning const { mergeConfig } = await import('vite') + const { default: tailwindcss } = await import('@tailwindcss/vite') // Filter out any plugins that might generate import maps if (config.plugins) { - config.plugins = config.plugins.filter((plugin: any) => { - if (plugin && plugin.name && plugin.name.includes('import-map')) { - return false - } - return true - }) + config.plugins = config.plugins + // Type guard: ensure we have valid plugin objects with names + .filter( + (plugin): plugin is NonNullable & { name: string } => { + return ( + plugin !== null && + plugin !== undefined && + typeof plugin === 'object' && + 'name' in plugin && + typeof plugin.name === 'string' + ) + } + ) + // Business logic: filter out import-map plugins + .filter((plugin) => !plugin.name.includes('import-map')) } return mergeConfig(config, { // Replace plugins entirely to avoid inheritance issues plugins: [ // Only include plugins we explicitly need for Storybook + tailwindcss(), Icons({ compiler: 'vue3', customCollections: { comfy: FileSystemIconLoader( - process.cwd() + '/src/assets/icons/custom' + process.cwd() + '/packages/design-system/src/icons' ) } }), diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index ae97c82dd..05e082ef0 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -4,17 +4,26 @@ transition: background-color 0.3s ease, color 0.3s ease; } - /* Light theme default */ - body { - background-color: #ffffff; - color: #1a1a1a; + /* Light theme default - with explicit color to override media queries */ + body:not(.dark-theme) { + background-color: #fff !important; + color: #000 !important; + } + + /* Override browser dark mode preference for light theme */ + @media (prefers-color-scheme: dark) { + body:not(.dark-theme) { + color: #000 !important; + --fg-color: #000 !important; + --bg-color: #fff !important; + } } /* Dark theme styles */ body.dark-theme, .dark-theme body { - background-color: #0a0a0a; - color: #e5e5e5; + background-color: #202020; + color: #fff; } /* Ensure Storybook canvas follows theme */ @@ -24,11 +33,32 @@ .dark-theme .sb-show-main, .dark-theme .docs-story { - background-color: #0a0a0a !important; + background-color: #202020 !important; } - /* Fix for Storybook controls panel in dark mode */ - .dark-theme .docblock-argstable-body { - color: #e5e5e5; + /* CSS Variables for theme consistency */ + body:not(.dark-theme) { + --fg-color: #000; + --bg-color: #fff; + --content-bg: #e0e0e0; + --content-fg: #000; + --content-hover-bg: #adadad; + --content-hover-fg: #000; + } + + body.dark-theme { + --fg-color: #fff; + --bg-color: #202020; + --content-bg: #4e4e4e; + --content-fg: #fff; + --content-hover-bg: #222; + --content-hover-fg: #fff; + } + + /* Override Storybook's problematic & selector styles */ + /* Reset only the specific properties that Storybook injects */ + li+li { + margin: 0; + padding: revert-layer; } \ No newline at end of file diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 747bbe802..bfe81f431 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,7 +1,7 @@ import { definePreset } from '@primevue/themes' import Aura from '@primevue/themes/aura' import { setup } from '@storybook/vue3' -import type { Preview } from '@storybook/vue3-vite' +import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite' import { createPinia } from 'pinia' import 'primeicons/primeicons.css' import PrimeVue from 'primevue/config' @@ -9,11 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice' import ToastService from 'primevue/toastservice' import Tooltip from 'primevue/tooltip' -import '../src/assets/css/style.css' -import { i18n } from '../src/i18n' -import '../src/lib/litegraph/public/css/litegraph.css' -import { useWidgetStore } from '../src/stores/widgetStore' -import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore' +import '@/assets/css/style.css' +import { i18n } from '@/i18n' +import '@/lib/litegraph/public/css/litegraph.css' const ComfyUIPreset = definePreset(Aura, { semantic: { @@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, { // Setup Vue app for Storybook setup((app) => { app.directive('tooltip', Tooltip) + + // Create Pinia instance const pinia = createPinia() + app.use(pinia) - - // Initialize stores - useColorPaletteStore(pinia) - useWidgetStore(pinia) - app.use(i18n) app.use(PrimeVue, { theme: { @@ -50,8 +46,8 @@ setup((app) => { app.use(ToastService) }) -// Dark theme decorator -export const withTheme = (Story: any, context: any) => { +// Theme and dialog decorator +export const withTheme = (Story: StoryFn, context: StoryContext) => { const theme = context.globals.theme || 'light' // Apply theme class to document root @@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => { document.body.classList.remove('dark-theme') } - return Story() + return Story(context.args, context) } const preview: Preview = { diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json index 96a1f5797..4efd1d966 100644 --- a/.vscode/tailwind.json +++ b/.vscode/tailwind.json @@ -2,12 +2,32 @@ "version": 1.1, "atDirectives": [ { - "name": "@tailwind", - "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", + "name": "@import", + "description": "Use the `@import` directive to inline CSS files, including Tailwind itself, into your stylesheet.", "references": [ { "name": "Tailwind Documentation", - "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" + "url": "https://tailwindcss.com/docs/functions-and-directives#import" + } + ] + }, + { + "name": "@theme", + "description": "Use the `@theme` directive to define custom design tokens like fonts, colors, and breakpoints.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#theme" + } + ] + }, + { + "name": "@layer", + "description": "Use the `@layer` directive inside `@theme` to organize custom styles into different layers like `base`, `components`, and `utilities`.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#layer" } ] }, @@ -22,32 +42,32 @@ ] }, { - "name": "@responsive", - "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", + "name": "@config", + "description": "Use the `@config` directive to load a legacy JavaScript-based Tailwind configuration file.", "references": [ { "name": "Tailwind Documentation", - "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" + "url": "https://tailwindcss.com/docs/functions-and-directives#config" } ] }, { - "name": "@screen", - "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\nâ€Ļgets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", + "name": "@reference", + "description": "Use the `@reference` directive to import theme variables, custom utilities, and custom variants from other files without duplicating CSS.", "references": [ { "name": "Tailwind Documentation", - "url": "https://tailwindcss.com/docs/functions-and-directives#screen" + "url": "https://tailwindcss.com/docs/functions-and-directives#reference" } ] }, { - "name": "@variants", - "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", + "name": "@plugin", + "description": "Use the `@plugin` directive to load a legacy JavaScript-based Tailwind plugin.", "references": [ { "name": "Tailwind Documentation", - "url": "https://tailwindcss.com/docs/functions-and-directives#variants" + "url": "https://tailwindcss.com/docs/functions-and-directives#plugin" } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 922d1f853..68be11a12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,22 +1,52 @@ # ComfyUI Frontend Project Guidelines +## Repository Setup + +For first-time setup, use the Claude command: +``` +/setup_repo +``` +This bootstraps the monorepo with dependencies, builds, tests, and dev server verification. + +**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006) + ## Quick Commands - `pnpm`: See all available commands +- `pnpm dev`: Start development server (port 5173, via nx) - `pnpm typecheck`: Type checking -- `pnpm lint`: Linting +- `pnpm build`: Build for production (via nx) +- `pnpm lint`: Linting (via nx) - `pnpm format`: Prettier formatting - `pnpm test:component`: Run component tests with browser environment - `pnpm test:unit`: Run all unit tests +- `pnpm test:browser`: Run E2E tests via Playwright - `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file +- `pnpm storybook`: Start Storybook development server (port 6006) +- `pnpm knip`: Detect unused code and dependencies + +## Monorepo Architecture + +The project now uses **Nx** for build orchestration and task management: + +- **Task Orchestration**: Commands like `dev`, `build`, `lint`, and `test:browser` run via Nx +- **Caching**: Nx provides intelligent caching for faster rebuilds +- **Configuration**: Managed through `nx.json` with plugins for ESLint, Storybook, Vite, and Playwright +- **Dependencies**: Nx handles dependency graph analysis and parallel execution + +Key Nx features: +- Build target caching and incremental builds +- Parallel task execution across the monorepo +- Plugin-based architecture for different tools ## Development Workflow -1. Make code changes -2. Run tests (see subdirectory CLAUDE.md files) -3. Run typecheck, lint, format -4. Check README updates -5. Consider docs.comfy.org updates +1. **First-time setup**: Run `/setup_repo` Claude command +2. Make code changes +3. Run tests (see subdirectory CLAUDE.md files) +4. Run typecheck, lint, format +5. Check README updates +6. Consider docs.comfy.org updates ## Git Conventions @@ -52,6 +82,44 @@ When referencing Comfy-Org repos: 2. Use GitHub API for branches/PRs/metadata 3. Curl GitHub website if needed +## Settings and Feature Flags Quick Reference + +### Settings Usage +```typescript +const settingStore = useSettingStore() +const value = settingStore.get('Comfy.SomeSetting') // Get setting +await settingStore.set('Comfy.SomeSetting', newValue) // Update setting +``` + +### Dynamic Defaults +```typescript +{ + id: 'Comfy.Example.Setting', + defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context +} +``` + +### Version-Based Defaults +```typescript +{ + id: 'Comfy.Example.Feature', + defaultValue: 'legacy', + defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout +} +``` + +### Feature Flags +```typescript +if (api.serverSupportsFeature('feature_name')) { // Check capability + // Use enhanced feature +} +const value = api.getServerFeature('config_name', defaultValue) // Get config +``` + +**Documentation:** +- Settings system: `docs/SETTINGS.md` +- Feature flags system: `docs/FEATURE_FLAGS.md` + ## Common Pitfalls - NEVER use `any` type - use proper TypeScript types @@ -59,3 +127,6 @@ When referencing Comfy-Org repos: - NEVER use `--no-verify` flag when committing - NEVER delete or disable tests to make them pass - NEVER circumvent quality checks +- 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: `
` + diff --git a/CODEOWNERS b/CODEOWNERS index 6abaa5b91..cd1b4e508 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,17 +1,61 @@ -# Admins -* @Comfy-Org/comfy_frontend_devs +# Desktop/Electron +/src/types/desktop/ @webfiltered +/src/constants/desktopDialogs.ts @webfiltered +/src/constants/desktopMaintenanceTasks.ts @webfiltered +/src/stores/electronDownloadStore.ts @webfiltered +/src/extensions/core/electronAdapter.ts @webfiltered +/src/views/DesktopDialogView.vue @webfiltered +/src/components/install/ @webfiltered +/src/components/maintenance/ @webfiltered +/vite.electron.config.mts @webfiltered -# Maintainers -*.md @Comfy-Org/comfy_maintainer -/tests-ui/ @Comfy-Org/comfy_maintainer -/browser_tests/ @Comfy-Org/comfy_maintainer -/.env_example @Comfy-Org/comfy_maintainer +# Common UI Components +/src/components/chip/ @viva-jinyi +/src/components/card/ @viva-jinyi +/src/components/button/ @viva-jinyi +/src/components/input/ @viva-jinyi -# Translations (AIGODLIKE team + shinshin86) -/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer +# Topbar +/src/components/topbar/ @pythongosssss -# Load 3D extension -/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs +# Thumbnail +/src/renderer/core/thumbnail/ @pythongosssss -# Mask Editor extension -/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs +# Legacy UI +/scripts/ui/ @pythongosssss + +# Link rendering +/src/renderer/core/canvas/links/ @benceruleanlu + +# Node help system +/src/utils/nodeHelpUtil.ts @benceruleanlu +/src/stores/workspace/nodeHelpStore.ts @benceruleanlu +/src/services/nodeHelpService.ts @benceruleanlu + +# Selection toolbox +/src/components/graph/selectionToolbox/ @Myestery + +# Minimap +/src/renderer/extensions/minimap/ @jtydhr88 + +# Assets +/src/platform/assets/ @arjansingh + +# Workflow Templates +/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki +/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki + +# Mask Editor +/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp +/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp +/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp + +# 3D +/src/extensions/core/load3d.ts @jtydhr88 +/src/components/load3d/ @jtydhr88 + +# Manager +/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata + +# Translations +/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83b1951bc..6614fe619 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat! ### Prerequisites & Technology Stack - **Required Software**: - - Node.js (v16 or later; v24 strongly recommended) and pnpm + - Node.js (v18 or later to build; v24 for vite dev server) and pnpm - Git for version control - A running ComfyUI backend instance @@ -265,9 +265,9 @@ The project supports three types of icons, all with automatic imports (no manual 2. **Iconify Icons** - 200,000+ icons from various libraries: ``, `` 3. **Custom Icons** - Your own SVG icons: `` -Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation. +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. -For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md). +For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md). ## Working with litegraph.js diff --git a/browser_tests/README.md b/browser_tests/README.md index ede6a303a..4954ba9fd 100644 --- a/browser_tests/README.md +++ b/browser_tests/README.md @@ -16,15 +16,20 @@ Without this flag, parallel tests will conflict and fail randomly. ### ComfyUI devtools -Clone to your `custom_nodes` directory. +ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory. _ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._ +For local development, copy the devtools files to your ComfyUI installation: +```bash +cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/ +``` + ### Node.js & Playwright Prerequisites Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver: ```bash -npx playwright install chromium --with-deps +pnpm exec playwright install chromium --with-deps ``` ### Environment Configuration @@ -51,14 +56,6 @@ TEST_COMFYUI_DIR=/path/to/your/ComfyUI ### Common Setup Issues -**Most tests require the new menu system** - Add to your test: - -```typescript -test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') -}) -``` - ### Release API Mocking By default, all tests mock the release API (`api.comfy.org/releases`) to prevent release notification popups from interfering with test execution. This is necessary because the release notifications can appear over UI elements and block test interactions. @@ -76,7 +73,7 @@ For tests that specifically need to test release functionality, see the example **Always use UI mode for development:** ```bash -npx playwright test --ui +pnpm exec playwright test --ui ``` UI mode features: @@ -92,8 +89,8 @@ UI mode features: For CI or headless testing: ```bash -npx playwright test # Run all tests -npx playwright test widget.spec.ts # Run specific test file +pnpm exec playwright test # Run all tests +pnpm exec playwright test widget.spec.ts # Run specific test file ``` ### Local Development Config @@ -389,7 +386,7 @@ export default defineConfig({ Option 2 - Generate local baselines for comparison: ```bash -npx playwright test --update-snapshots +pnpm exec playwright test --update-snapshots ``` ### Creating New Screenshot Baselines diff --git a/browser_tests/assets/vueNodes/simple-triple.json b/browser_tests/assets/vueNodes/simple-triple.json new file mode 100644 index 000000000..9b665191d --- /dev/null +++ b/browser_tests/assets/vueNodes/simple-triple.json @@ -0,0 +1 @@ +{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4} \ No newline at end of file diff --git a/browser_tests/assets/workflowInMedia/workflow.glb b/browser_tests/assets/workflowInMedia/workflow.glb index 725727e65..f033e9c6f 100644 Binary files a/browser_tests/assets/workflowInMedia/workflow.glb and b/browser_tests/assets/workflowInMedia/workflow.glb differ diff --git a/browser_tests/fixtures/ComfyMouse.ts b/browser_tests/fixtures/ComfyMouse.ts index 306f4352b..dfb0281eb 100644 --- a/browser_tests/fixtures/ComfyMouse.ts +++ b/browser_tests/fixtures/ComfyMouse.ts @@ -10,7 +10,7 @@ import type { Position } from './types' * - {@link Mouse.move} * - {@link Mouse.up} */ -export interface DragOptions { +interface DragOptions { button?: 'left' | 'right' | 'middle' clickCount?: number steps?: number diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index f64ca5c94..19796f4c4 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -5,13 +5,14 @@ import dotenv from 'dotenv' import * as fs from 'fs' import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph' -import type { NodeId } from '../../src/schemas/comfyWorkflowSchema' +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' import { @@ -144,6 +145,7 @@ export class ComfyPage { public readonly templates: ComfyTemplates public readonly settingDialog: SettingDialog public readonly confirmDialog: ConfirmDialog + public readonly vueNodes: VueNodeHelpers /** Worker index to test user ID */ public readonly userIds: string[] = [] @@ -172,6 +174,7 @@ export class ComfyPage { this.templates = new ComfyTemplates(page) this.settingDialog = new SettingDialog(page, this) this.confirmDialog = new ConfirmDialog(page) + this.vueNodes = new VueNodeHelpers(page) } convertLeafToContent(structure: FolderStructure): FolderStructure { @@ -453,6 +456,32 @@ export class ComfyPage { await workflowsTab.close() } + /** + * Attach a screenshot to the test report. + * By default, screenshots are only taken in non-CI environments. + * @param name - Name for the screenshot attachment + * @param options - Optional configuration + * @param options.runInCI - Whether to take screenshot in CI (default: false) + * @param options.fullPage - Whether to capture full page (default: false) + */ + async attachScreenshot( + name: string, + options: { runInCI?: boolean; fullPage?: boolean } = {} + ) { + const { runInCI = false, fullPage = false } = options + + // Skip in CI unless explicitly requested + if (process.env.CI && !runInCI) { + return + } + + const testInfo = comfyPageFixture.info() + await testInfo.attach(name, { + body: await this.page.screenshot({ fullPage }), + contentType: 'image/png' + }) + } + async resetView() { if (await this.resetViewButton.isVisible()) { await this.resetViewButton.click() @@ -1395,7 +1424,7 @@ export class ComfyPage { } async closeDialog() { - await this.page.locator('.p-dialog-close-button').click() + await this.page.locator('.p-dialog-close-button').click({ force: true }) await expect(this.page.locator('.p-dialog')).toBeHidden() } @@ -1614,7 +1643,7 @@ export const comfyPageFixture = base.extend<{ try { await comfyPage.setupSettings({ - 'Comfy.UseNewMenu': 'Disabled', + 'Comfy.UseNewMenu': 'Top', // Hide canvas menu/info/selection toolbox by default. 'Comfy.Graph.CanvasInfo': false, 'Comfy.Graph.CanvasMenu': false, diff --git a/browser_tests/fixtures/UserSelectPage.ts b/browser_tests/fixtures/UserSelectPage.ts index c0d4c90d3..ff0735e17 100644 --- a/browser_tests/fixtures/UserSelectPage.ts +++ b/browser_tests/fixtures/UserSelectPage.ts @@ -1,5 +1,5 @@ +import type { Page } from '@playwright/test' import { test as base } from '@playwright/test' -import { Page } from 'playwright' export class UserSelectPage { constructor( diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts new file mode 100644 index 000000000..b51750299 --- /dev/null +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -0,0 +1,117 @@ +/** + * Vue Node Test Helpers + */ +import type { Locator, Page } from '@playwright/test' + +export class VueNodeHelpers { + constructor(private page: Page) {} + + /** + * Get locator for all Vue node components in the DOM + */ + get nodes(): Locator { + return this.page.locator('[data-node-id]') + } + + /** + * Get locator for selected Vue node components (using visual selection indicators) + */ + get selectedNodes(): Locator { + return this.page.locator( + '[data-node-id].outline-black, [data-node-id].outline-white' + ) + } + + /** + * Get locator for a Vue node by the node's title (displayed name in the header) + */ + getNodeByTitle(title: string): Locator { + return this.page.locator(`[data-node-id]`).filter({ hasText: title }) + } + + /** + * Get total count of Vue nodes in the DOM + */ + async getNodeCount(): Promise { + return await this.nodes.count() + } + + /** + * Get count of selected Vue nodes + */ + async getSelectedNodeCount(): Promise { + return await this.selectedNodes.count() + } + + /** + * Get all Vue node IDs currently in the DOM + */ + async getNodeIds(): Promise { + return await this.nodes.evaluateAll((nodes) => + nodes + .map((n) => n.getAttribute('data-node-id')) + .filter((id): id is string => id !== null) + ) + } + + /** + * Select a specific Vue node by ID + */ + async selectNode(nodeId: string): Promise { + await this.page.locator(`[data-node-id="${nodeId}"]`).click() + } + + /** + * Select multiple Vue nodes by IDs using Ctrl+click + */ + async selectNodes(nodeIds: string[]): Promise { + if (nodeIds.length === 0) return + + // Select first node normally + await this.selectNode(nodeIds[0]) + + // Add additional nodes with Ctrl+click + for (let i = 1; i < nodeIds.length; i++) { + await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({ + modifiers: ['Control'] + }) + } + } + + /** + * Clear all selections by clicking empty space + */ + async clearSelection(): Promise { + await this.page.mouse.click(50, 50) + } + + /** + * Delete selected Vue nodes using Delete key + */ + async deleteSelected(): Promise { + await this.page.locator('#graph-canvas').focus() + await this.page.keyboard.press('Delete') + } + + /** + * Delete selected Vue nodes using Backspace key + */ + async deleteSelectedWithBackspace(): Promise { + await this.page.locator('#graph-canvas').focus() + await this.page.keyboard.press('Backspace') + } + + /** + * Wait for Vue nodes to be rendered + */ + async waitForNodes(expectedCount?: number): Promise { + if (expectedCount !== undefined) { + await this.page.waitForFunction( + (count) => document.querySelectorAll('[data-node-id]').length >= count, + expectedCount + ) + } else { + await this.page.waitForSelector('[data-node-id]') + } + } +} diff --git a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts index 23dc104cf..fd40ca911 100644 --- a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts +++ b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' export class ComfyNodeSearchFilterSelectionPanel { constructor(public readonly page: Page) {} diff --git a/browser_tests/fixtures/components/SettingDialog.ts b/browser_tests/fixtures/components/SettingDialog.ts index afaf86154..e9040a3a9 100644 --- a/browser_tests/fixtures/components/SettingDialog.ts +++ b/browser_tests/fixtures/components/SettingDialog.ts @@ -1,6 +1,6 @@ -import { Page } from '@playwright/test' +import type { Page } from '@playwright/test' -import { ComfyPage } from '../ComfyPage' +import type { ComfyPage } from '../ComfyPage' export class SettingDialog { constructor( diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 7baaa1ef9..f3fbe42cf 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' class SidebarTab { constructor( diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts index f2c9dfa16..6d0cd1fb3 100644 --- a/browser_tests/fixtures/components/Topbar.ts +++ b/browser_tests/fixtures/components/Topbar.ts @@ -1,7 +1,14 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' export class Topbar { - constructor(public readonly page: Page) {} + private readonly menuLocator: Locator + private readonly menuTrigger: Locator + + constructor(public readonly page: Page) { + this.menuLocator = page.locator('.comfy-command-menu') + this.menuTrigger = page.locator('.comfyui-logo-wrapper') + } async getTabNames(): Promise { return await this.page @@ -15,10 +22,33 @@ export class Topbar { .innerText() } - getMenuItem(itemLabel: string): Locator { + /** + * Get a menu item by its label, optionally within a specific parent container + */ + getMenuItem(itemLabel: string, parent?: Locator): Locator { + if (parent) { + return parent.locator(`.p-tieredmenu-item:has-text("${itemLabel}")`) + } + return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`) } + /** + * Get the visible submenu (last visible submenu in case of nested menus) + */ + getVisibleSubmenu(): Locator { + return this.page.locator('.p-tieredmenu-submenu:visible').last() + } + + /** + * Check if a menu item has an active checkmark + */ + async isMenuItemActive(menuItem: Locator): Promise { + const checkmark = menuItem.locator('.pi-check') + const classes = await checkmark.getAttribute('class') + return classes ? !classes.includes('invisible') : false + } + getWorkflowTab(tabName: string): Locator { return this.page .locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`) @@ -66,10 +96,50 @@ export class Topbar { async openTopbarMenu() { await this.page.waitForTimeout(1000) - await this.page.locator('.comfyui-logo-wrapper').click() - const menu = this.page.locator('.comfy-command-menu') - await menu.waitFor({ state: 'visible' }) - return menu + await this.menuTrigger.click() + await this.menuLocator.waitFor({ state: 'visible' }) + return this.menuLocator + } + + /** + * Close the topbar menu by clicking outside + */ + async closeTopbarMenu() { + await this.page.locator('body').click({ position: { x: 10, y: 10 } }) + await expect(this.menuLocator).not.toBeVisible() + } + + /** + * Navigate to a submenu by hovering over a menu item + */ + async openSubmenu(menuItemLabel: string): Promise { + const menuItem = this.getMenuItem(menuItemLabel) + await menuItem.hover() + const submenu = this.getVisibleSubmenu() + await submenu.waitFor({ state: 'visible' }) + return submenu + } + + /** + * Get theme menu items and interact with theme switching + */ + async getThemeMenuItems() { + const themeSubmenu = await this.openSubmenu('Theme') + return { + submenu: themeSubmenu, + darkTheme: this.getMenuItem('Dark (Default)', themeSubmenu), + lightTheme: this.getMenuItem('Light', themeSubmenu) + } + } + + /** + * Switch to a specific theme + */ + async switchTheme(theme: 'dark' | 'light') { + const { darkTheme, lightTheme } = await this.getThemeMenuItems() + const themeItem = theme === 'dark' ? darkTheme : lightTheme + const themeLabel = themeItem.locator('.p-menubar-item-label') + await themeLabel.click() } async triggerTopbarCommand(path: string[]) { @@ -79,9 +149,7 @@ export class Topbar { const menu = await this.openTopbarMenu() const tabName = path[0] - const topLevelMenuItem = this.page.locator( - `.p-menubar-item-label:text-is("${tabName}")` - ) + const topLevelMenuItem = this.getMenuItem(tabName) const topLevelMenu = menu .locator('.p-tieredmenu-item') .filter({ has: topLevelMenuItem }) diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index 8a52d8b66..4becc999c 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -1,6 +1,6 @@ import type { Page } from '@playwright/test' -import type { NodeId } from '../../../src/schemas/comfyWorkflowSchema' +import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema' import { ManageGroupNode } from '../../helpers/manageGroupNode' import type { ComfyPage } from '../ComfyPage' import type { Position, Size } from '../types' @@ -134,7 +134,7 @@ export class SubgraphSlotReference { } } -export class NodeSlotReference { +class NodeSlotReference { constructor( readonly type: 'input' | 'output', readonly index: number, @@ -201,7 +201,7 @@ export class NodeSlotReference { } } -export class NodeWidgetReference { +class NodeWidgetReference { constructor( readonly index: number, readonly node: NodeReference diff --git a/browser_tests/fixtures/utils/taskHistory.ts b/browser_tests/fixtures/utils/taskHistory.ts index 9b09df14e..7471ee74d 100644 --- a/browser_tests/fixtures/utils/taskHistory.ts +++ b/browser_tests/fixtures/utils/taskHistory.ts @@ -1,7 +1,7 @@ +import type { Request, Route } from '@playwright/test' import _ from 'es-toolkit/compat' import fs from 'fs' import path from 'path' -import type { Request, Route } from 'playwright' import { v4 as uuidv4 } from 'uuid' import type { diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts new file mode 100644 index 000000000..5c4541b92 --- /dev/null +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -0,0 +1,131 @@ +import type { Locator, Page } from '@playwright/test' + +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 nodeRef: NodeReference, + private readonly page: Page + ) {} + + /** + * Get the node's header element using data-testid + */ + async getHeader(): Promise { + const nodeId = this.nodeRef.id + return this.page.locator(`[data-testid="node-header-${nodeId}"]`) + } + + /** + * Get the node's title element + */ + async getTitleElement(): Promise { + const header = await this.getHeader() + return header.locator('[data-testid="node-title"]') + } + + /** + * Get the current title text + */ + async getTitle(): Promise { + const titleElement = await this.getTitleElement() + return (await titleElement.textContent()) || '' + } + + /** + * Set a new title by double-clicking and entering text + */ + async setTitle(newTitle: string): Promise { + 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 { + 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 { + 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 { + const header = await this.getHeader() + return header.locator('[data-testid="node-collapse-button"]') + } + + /** + * Toggle the node's collapsed state + */ + async toggleCollapse(): Promise { + const button = await this.getCollapseButton() + await button.click() + } + + /** + * Get the collapse icon element + */ + async getCollapseIcon(): Promise { + const button = await this.getCollapseButton() + return button.locator('i') + } + + /** + * Get the collapse icon's CSS classes + */ + async getCollapseIconClass(): Promise { + const icon = await this.getCollapseIcon() + return (await icon.getAttribute('class')) || '' + } + + /** + * Check if the collapse button is visible + */ + async isCollapseButtonVisible(): Promise { + const button = await this.getCollapseButton() + return await button.isVisible() + } + + /** + * Get the node's body/content element + */ + async getBody(): Promise { + 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 { + const body = await this.getBody() + return await body.isVisible() + } +} diff --git a/browser_tests/fixtures/ws.ts b/browser_tests/fixtures/ws.ts index e12c53465..f1ab1a538 100644 --- a/browser_tests/fixtures/ws.ts +++ b/browser_tests/fixtures/ws.ts @@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{ // so we can look it up to trigger messages const store: Record = ((window as any).__ws__ = {}) window.WebSocket = class extends window.WebSocket { - constructor() { - // @ts-expect-error - super(...arguments) + constructor( + ...rest: ConstructorParameters + ) { + super(...rest) store[this.url] = this } } diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts index 12033fce3..881ef11c4 100644 --- a/browser_tests/globalSetup.ts +++ b/browser_tests/globalSetup.ts @@ -1,4 +1,4 @@ -import { FullConfig } from '@playwright/test' +import type { FullConfig } from '@playwright/test' import dotenv from 'dotenv' import { backupPath } from './utils/backupUtils' diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts index 47bab3db9..aeed77294 100644 --- a/browser_tests/globalTeardown.ts +++ b/browser_tests/globalTeardown.ts @@ -1,4 +1,4 @@ -import { FullConfig } from '@playwright/test' +import type { FullConfig } from '@playwright/test' import dotenv from 'dotenv' import { restorePath } from './utils/backupUtils' diff --git a/browser_tests/helpers/fitToView.ts b/browser_tests/helpers/fitToView.ts new file mode 100644 index 000000000..af6c10e9d --- /dev/null +++ b/browser_tests/helpers/fitToView.ts @@ -0,0 +1,104 @@ +import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces' +import type { ComfyPage } from '../fixtures/ComfyPage' + +interface FitToViewOptions { + selectionOnly?: boolean + zoom?: number + padding?: number +} + +/** + * Instantly fits the canvas view to graph content without waiting for UI animation. + * + * Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented. + */ +export async function fitToViewInstant( + comfyPage: ComfyPage, + options: FitToViewOptions = {} +) { + const { selectionOnly = false, zoom = 0.75, padding = 10 } = options + + const rectangles = await comfyPage.page.evaluate< + ReadOnlyRect[] | null, + { selectionOnly: boolean } + >( + ({ selectionOnly }) => { + const app = window['app'] + if (!app?.canvas) return null + + const canvas = app.canvas + const items = (() => { + if (selectionOnly && canvas.selectedItems?.size) { + return Array.from(canvas.selectedItems) + } + try { + return Array.from(canvas.positionableItems ?? []) + } catch { + return [] + } + })() + + if (!items.length) return null + + const rects: ReadOnlyRect[] = [] + + for (const item of items) { + const rect = item?.boundingRect + if (!rect) continue + + const x = Number(rect[0]) + const y = Number(rect[1]) + const width = Number(rect[2]) + const height = Number(rect[3]) + + rects.push([x, y, width, height] as const) + } + + return rects.length ? rects : null + }, + { selectionOnly } + ) + + if (!rectangles || rectangles.length === 0) return + + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + + for (const [x, y, width, height] of rectangles) { + minX = Math.min(minX, Number(x)) + minY = Math.min(minY, Number(y)) + maxX = Math.max(maxX, Number(x) + Number(width)) + maxY = Math.max(maxY, Number(y) + Number(height)) + } + + const hasFiniteBounds = + Number.isFinite(minX) && + Number.isFinite(minY) && + Number.isFinite(maxX) && + Number.isFinite(maxY) + + if (!hasFiniteBounds) return + + const bounds: ReadOnlyRect = [ + minX - padding, + minY - padding, + maxX - minX + 2 * padding, + maxY - minY + 2 * padding + ] + + await comfyPage.page.evaluate( + ({ bounds, zoom }) => { + const app = window['app'] + if (!app?.canvas) return + + const canvas = app.canvas + canvas.ds.fitToBounds(bounds, { zoom }) + canvas.setDirty(true, true) + }, + { bounds, zoom } + ) + + await comfyPage.nextFrame() +} diff --git a/browser_tests/helpers/manageGroupNode.ts b/browser_tests/helpers/manageGroupNode.ts index a444a97c6..45010b979 100644 --- a/browser_tests/helpers/manageGroupNode.ts +++ b/browser_tests/helpers/manageGroupNode.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' export class ManageGroupNode { footer: Locator diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts index d659e125a..c690b8702 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -1,10 +1,10 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import path from 'path' -import { +import type { TemplateInfo, WorkflowTemplates -} from '../../src/types/workflowTemplateTypes' +} from '../../src/platform/workflow/templates/types/template' export class ComfyTemplates { readonly content: Locator diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index a504ea4fc..b23e4466d 100644 --- a/browser_tests/tests/actionbar.spec.ts +++ b/browser_tests/tests/actionbar.spec.ts @@ -29,9 +29,9 @@ test.describe('Actionbar', () => { // Intercept the prompt queue endpoint let promptNumber = 0 - comfyPage.page.route('**/api/prompt', async (route, req) => { + await comfyPage.page.route('**/api/prompt', async (route, req) => { await new Promise((r) => setTimeout(r, 100)) - route.fulfill({ + await route.fulfill({ status: 200, body: JSON.stringify({ prompt_id: promptNumber, diff --git a/browser_tests/tests/backgroundImageUpload.spec.ts b/browser_tests/tests/backgroundImageUpload.spec.ts index 24af9e8ac..7f3ed6a3d 100644 --- a/browser_tests/tests/backgroundImageUpload.spec.ts +++ b/browser_tests/tests/backgroundImageUpload.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Background Image Upload', () => { test.beforeEach(async ({ comfyPage }) => { // Reset the background image setting before each test diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts index 7a32833e4..8e39154f1 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -1,5 +1,5 @@ +import type { ComfyPage } from '../fixtures/ComfyPage' import { - ComfyPage, comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage' @@ -15,6 +15,10 @@ async function afterChange(comfyPage: ComfyPage) { }) } +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Change Tracker', () => { test.describe('Undo/Redo', () => { test.beforeEach(async ({ comfyPage }) => { diff --git a/browser_tests/tests/chatHistory.spec.ts b/browser_tests/tests/chatHistory.spec.ts index db3397514..c47a4d19b 100644 --- a/browser_tests/tests/chatHistory.spec.ts +++ b/browser_tests/tests/chatHistory.spec.ts @@ -1,7 +1,12 @@ -import { Page, expect } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + interface ChatHistoryEntry { prompt: string response: string diff --git a/browser_tests/tests/colorPalette.spec.ts b/browser_tests/tests/colorPalette.spec.ts index 901cce913..6dd53c194 100644 --- a/browser_tests/tests/colorPalette.spec.ts +++ b/browser_tests/tests/colorPalette.spec.ts @@ -3,6 +3,10 @@ import { expect } from '@playwright/test' import type { Palette } from '../../src/schemas/colorPaletteSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + const customColorPalettes: Record = { obsidian: { version: 102, diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png index 6bb2c6789..1ba61954e 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png index b0137473b..70c81dfbb 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png index 8457f572d..47083ae5c 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png index 0eceb7171..722020a96 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png index 6bb2c6789..1ba61954e 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png index 8099a716b..ee50ed0c5 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png index baf1db5ef..3d490143e 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png index 2c43b3303..f5eeaa61b 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png index ee5464dd9..6b0e3a30a 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png index 0eceb7171..722020a96 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png differ diff --git a/browser_tests/tests/commands.spec.ts b/browser_tests/tests/commands.spec.ts index 4225ad228..e271f2e15 100644 --- a/browser_tests/tests/commands.spec.ts +++ b/browser_tests/tests/commands.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Keybindings', () => { test('Should execute command', async ({ comfyPage }) => { await comfyPage.registerCommand('TestCommand', () => { diff --git a/browser_tests/tests/copyPaste.spec.ts b/browser_tests/tests/copyPaste.spec.ts index 3bcee65f0..cabb849e8 100644 --- a/browser_tests/tests/copyPaste.spec.ts +++ b/browser_tests/tests/copyPaste.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Copy Paste', () => { test('Can copy and paste node', async ({ comfyPage }) => { await comfyPage.clickEmptyLatentNode() diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png index b3dab7374..6f5edfd8f 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png index b96a1be9d..9a695eed8 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png index db35e0439..1c1a63047 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png index d2fd6a159..164773278 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png index 5e71f1867..be7214acf 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png index 363dd0222..fdc151be1 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png differ diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index bdfcd392f..7459acf58 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,8 +1,13 @@ -import { Locator, expect } from '@playwright/test' +import type { Locator } from '@playwright/test' +import { expect } from '@playwright/test' import type { Keybinding } from '../../src/schemas/keyBindingSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Load workflow warning', () => { test('Should display a warning when loading a workflow with missing nodes', async ({ comfyPage @@ -36,6 +41,10 @@ test('Does not report warning on undo/redo', async ({ comfyPage }) => { await comfyPage.loadWorkflow('missing/missing_nodes') await comfyPage.closeDialog() + // Wait for any async operations to complete after dialog closes + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(100) + // Make a change to the graph await comfyPage.doubleClickCanvas() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') @@ -59,18 +68,6 @@ test.describe('Execution error', () => { const executionError = comfyPage.page.locator('.comfy-error-report') await expect(executionError).toBeVisible() }) - - test('Can display Issue Report form', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/execution_error') - await comfyPage.queueButton.click() - await comfyPage.nextFrame() - - await comfyPage.page.getByLabel('Help Fix This').click() - const issueReportForm = comfyPage.page.getByText( - 'Submit Error Report (Optional)' - ) - await expect(issueReportForm).toBeVisible() - }) }) test.describe('Missing models warning', () => { @@ -303,37 +300,16 @@ test.describe('Settings', () => { }) }) -test.describe('Feedback dialog', () => { - test('Should open from topmenu help command', async ({ comfyPage }) => { - // Open feedback dialog from top menu +test.describe('Support', () => { + test('Should open external zendesk link', async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback']) + const pagePromise = comfyPage.page.context().waitForEvent('page') + await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support']) + const newPage = await pagePromise - // Verify feedback dialog content is visible - const feedbackHeader = comfyPage.page.getByRole('heading', { - name: 'Feedback' - }) - await expect(feedbackHeader).toBeVisible() - }) - - test('Should close when close button clicked', async ({ comfyPage }) => { - // Open feedback dialog - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback']) - - const feedbackHeader = comfyPage.page.getByRole('heading', { - name: 'Feedback' - }) - - // Close feedback dialog - await comfyPage.page - .getByLabel('', { exact: true }) - .getByLabel('Close') - .click() - await feedbackHeader.waitFor({ state: 'hidden' }) - - // Verify dialog is closed - await expect(feedbackHeader).not.toBeVisible() + await newPage.waitForLoadState('networkidle') + await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/) + await newPage.close() }) }) diff --git a/browser_tests/tests/domWidget.spec.ts b/browser_tests/tests/domWidget.spec.ts index 91d53c407..6517b9170 100644 --- a/browser_tests/tests/domWidget.spec.ts +++ b/browser_tests/tests/domWidget.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('DOM Widget', () => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/collapsed_multiline') diff --git a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png index 3acc073ff..2b89be5c5 100644 Binary files a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png and b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png differ diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index 4adab98b6..075025a3a 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Execution', () => { test('Report error on unconnected slot', async ({ comfyPage }) => { await comfyPage.disconnectEdge() diff --git a/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png b/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png index 2d4c66ef5..8b8287a40 100644 Binary files a/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png and b/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png differ diff --git a/browser_tests/tests/extensionAPI.spec.ts b/browser_tests/tests/extensionAPI.spec.ts index 7711ccf3b..38f4a6c1d 100644 --- a/browser_tests/tests/extensionAPI.spec.ts +++ b/browser_tests/tests/extensionAPI.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { SettingParams } from '../../src/types/settingTypes' +import type { SettingParams } from '../../src/platform/settings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Topbar commands', () => { @@ -247,7 +247,7 @@ test.describe('Topbar commands', () => { test.describe('Dialog', () => { test('Should allow showing a prompt dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].extensionManager.dialog + void window['app'].extensionManager.dialog .prompt({ title: 'Test Prompt', message: 'Test Prompt Message' @@ -267,7 +267,7 @@ test.describe('Topbar commands', () => { comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].extensionManager.dialog + void window['app'].extensionManager.dialog .confirm({ title: 'Test Confirm', message: 'Test Confirm Message' @@ -284,7 +284,7 @@ test.describe('Topbar commands', () => { test('Should allow dismissing a dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { window['value'] = 'foo' - window['app'].extensionManager.dialog + void window['app'].extensionManager.dialog .confirm({ title: 'Test Confirm', message: 'Test Confirm Message' diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 73eb35f47..38286b399 100644 --- a/browser_tests/tests/featureFlags.spec.ts +++ b/browser_tests/tests/featureFlags.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Feature Flags', () => { test('Client and server exchange feature flags on connection', async ({ comfyPage diff --git a/browser_tests/tests/graph.spec.ts b/browser_tests/tests/graph.spec.ts index 25e166bab..cd89e92d5 100644 --- a/browser_tests/tests/graph.spec.ts +++ b/browser_tests/tests/graph.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Graph', () => { // Should be able to fix link input slot index after swap the input order // Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348 diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index 9ae090975..daa165fa4 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Graph Canvas Menu', () => { test.beforeEach(async ({ comfyPage }) => { // Set link render mode to spline to make sure it's not affected by other tests' diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png index 72749caf3..2736a50c5 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png differ diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png index 3d86bdfc0..fd72c2d0a 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png differ diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index 41b50224a..9a2310231 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -1,8 +1,13 @@ import { expect } from '@playwright/test' -import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Group Node', () => { test.describe('Node library sidebar', () => { const groupNodeName = 'DefautWorkflowGroupNode' diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index de46bca2e..2fc753490 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -1,12 +1,17 @@ -import { Locator, expect } from '@playwright/test' -import { Position } from '@vueuse/core' +import type { Locator } from '@playwright/test' +import { expect } from '@playwright/test' +import type { Position } from '@vueuse/core' import { type ComfyPage, comfyPageFixture as test, testComfySnapToGridGridSize } from '../fixtures/ComfyPage' -import { type NodeReference } from '../fixtures/utils/litegraphUtils' +import type { NodeReference } from '../fixtures/utils/litegraphUtils' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) test.describe('Item Interaction', () => { test('Can select/delete all items', async ({ comfyPage }) => { @@ -1012,6 +1017,8 @@ test.describe('Canvas Navigation', () => { test('Shift + mouse wheel should pan canvas horizontally', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning') + await comfyPage.page.click('canvas') await comfyPage.nextFrame() diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png index 6047baa39..e2e2f2206 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png index 2111c1aa0..27c987620 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png index 3aeda00d7..5732eea2b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png index 5f2617b5b..3ebdeb306 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png index ab932fa29..ba16c1a37 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/deleted-all-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/deleted-all-chromium-linux.png index 0d2635ee3..fd4b5a115 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/deleted-all-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/deleted-all-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png index 40fda2b55..c07494c21 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png index 72ffbfdcd..a3d7a8443 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png index 231bc8179..b8e5cdab9 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png index 2a2ca815e..e425d88e8 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/group-fit-to-contents-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/group-fit-to-contents-chromium-linux.png index 642695b4f..99bd797a8 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/group-fit-to-contents-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/group-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png index 603c598ad..3e8474a2b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/group-title-edited-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/group-title-edited-chromium-linux.png index a9f9c918a..082281496 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/group-title-edited-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/group-title-edited-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-alt-shift-drag-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-alt-shift-drag-chromium-linux.png index 36cd31910..750d15c1b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-alt-shift-drag-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-alt-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-click-node-select-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-click-node-select-chromium-linux.png index 0be4a3565..d0bd046ad 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-click-node-select-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-click-node-select-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-left-drag-pan-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-left-drag-pan-chromium-linux.png index 36cd31910..750d15c1b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-left-drag-pan-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-left-drag-pan-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-middle-drag-pan-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-middle-drag-pan-chromium-linux.png index 36cd31910..750d15c1b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-middle-drag-pan-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-middle-drag-pan-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-in-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-in-chromium-linux.png index 5023002f4..c457dea5c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-in-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-in-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-out-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-out-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-out-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/legacy-wheel-zoom-out-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png index ddc49ca6a..7a631a02f 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png index 2b55f1308..8255f0f5a 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png index 7cc712f74..7181cdcfc 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png index 895e429a1..f6b135cb6 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png index a37ea3f8c..a6d09ff18 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png index a37ea3f8c..a6d09ff18 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-from-far-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-from-far-chromium-linux.png index 83d2c7869..7445cc3f9 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-from-far-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-from-far-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-two-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-two-chromium-linux.png index f8948b451..5b2380510 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-two-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-two-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-chromium-linux.png index b0853d8d2..02d3823e4 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-far-away-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-far-away-chromium-linux.png index 37e35114b..fd4b5a115 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-far-away-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-far-away-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-one-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-one-chromium-linux.png index f8948b451..5b2380510 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-one-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-one-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-two-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-two-chromium-linux.png index 83d2c7869..7445cc3f9 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-two-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-step-two-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-touch-mobile-chrome-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-touch-mobile-chrome-linux.png index 35d3db2b6..89cdd22d2 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-touch-mobile-chrome-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png index 6b9a31213..6d06644e3 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/pinned-all-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/pinned-all-chromium-linux.png index 0fbb5bbb6..51f5fc50e 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/pinned-all-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/pinned-all-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png index cdccaf8c3..a8fed2ac0 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png index ae8690075..6811cb646 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-text-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-text-chromium-linux.png index e0bcb17ab..6ce3ce119 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-text-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-text-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-all-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-all-chromium-linux.png index 0a90522c1..a149cb8ef 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-all-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-all-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png index c17b1af9f..f57ca3be8 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png index 0be4a3565..d0bd046ad 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png index e715bc540..9e998d26f 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png index d07c59105..f76d1945d 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-chromium-linux.png index c297a28e9..797b10f6f 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png index c297a28e9..797b10f6f 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-modified-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-modified-chromium-linux.png index 4304cbd83..1dfcfa4fe 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-modified-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-modified-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-chromium-linux.png index 52af479fe..dc1258e21 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-linked-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-linked-chromium-linux.png index 283760b20..2a80227ab 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-linked-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/snap-to-slot-linked-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png index 7310c14b0..37866d892 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-click-node-select-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-click-node-select-chromium-linux.png index 0be4a3565..d0bd046ad 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-click-node-select-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-click-node-select-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-in-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-in-chromium-linux.png index 5023002f4..c457dea5c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-in-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-in-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-out-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-out-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-out-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-ctrl-wheel-zoom-out-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-initial-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-initial-chromium-linux.png index c09736577..a9d0efb74 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-initial-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-initial-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png index a37ea3f8c..a6d09ff18 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-middle-drag-pan-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-middle-drag-pan-chromium-linux.png index 36cd31910..750d15c1b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-middle-drag-pan-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-middle-drag-pan-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png index c9d79512f..a9d0efb74 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png index c09736577..57a92edc5 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png index c9d79512f..e607294e3 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-space-drag-pan-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-space-drag-pan-chromium-linux.png index d9d922bd3..3db5f8a8c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-space-drag-pan-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-space-drag-pan-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/string-input-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/string-input-chromium-linux.png index ed2e4a01f..2c3343f6b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/string-input-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/string-input-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/string-node-id-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/string-node-id-chromium-linux.png index bc46d79f4..7b7b8f84a 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/string-node-id-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/string-node-id-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png index fb5a7043a..67a37ae57 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png index 6d9af3687..dfd279b57 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/unpinned-all-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/unpinned-all-chromium-linux.png index 0a90522c1..a149cb8ef 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/unpinned-all-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/unpinned-all-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png index 36bb23516..3d891b985 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png index 27ec24457..39ec12552 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index 19b62813e..ef8c2d9b2 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-high-zoom-speed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-high-zoom-speed-chromium-linux.png index 1c22405e6..4be78a0bd 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-high-zoom-speed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-high-zoom-speed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png index 94e5dda53..a3f68aaf7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-ctrl-shift-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-ctrl-shift-chromium-linux.png index 1a2a25279..910bf98e6 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-ctrl-shift-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-high-zoom-speed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-high-zoom-speed-chromium-linux.png index 8f0ed5716..27b3ff339 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-high-zoom-speed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-high-zoom-speed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png index 4a69f0422..a135084e8 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-very-far-out-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-very-far-out-chromium-linux.png index f91becc0e..e807682d3 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-very-far-out-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-very-far-out-chromium-linux.png differ diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts index ced293637..f4244ae66 100644 --- a/browser_tests/tests/keybindings.spec.ts +++ b/browser_tests/tests/keybindings.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Keybindings', () => { test('Should not trigger non-modifier keybinding when typing in input fields', async ({ comfyPage diff --git a/browser_tests/tests/litegraphEvent.spec.ts b/browser_tests/tests/litegraphEvent.spec.ts index 8d8f6c2e8..184943fe0 100644 --- a/browser_tests/tests/litegraphEvent.spec.ts +++ b/browser_tests/tests/litegraphEvent.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + function listenForEvent(): Promise { return new Promise((resolve) => { document.addEventListener('litegraph:canvas', (e) => resolve(e), { diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 92fa8dd9d..f091058d2 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Load Workflow in Media', () => { const fileNames = [ 'workflow.webp', @@ -15,8 +19,10 @@ test.describe('Load Workflow in Media', () => { 'workflow.mp4', 'workflow.mov', 'workflow.m4v', - 'workflow.svg', - 'workflow.avif' + 'workflow.svg' + // TODO: Re-enable after fixing test asset to use core nodes only + // Currently opens missing nodes dialog which is outside scope of AVIF loading functionality + // 'workflow.avif' ] fileNames.forEach(async (fileName) => { test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png index 34067130e..eb4903bf8 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png index 2c47fac3a..2e327dff0 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png index 893d1196d..2ff59b391 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png index 4a4bea714..8f347b607 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png index 12e526ce6..9ca4c0fab 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png index 0a06172ed..65446a6c7 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png index 0a06172ed..65446a6c7 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png index 0a06172ed..65446a6c7 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png index 2c47fac3a..2e327dff0 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png index f5285ca2e..58e7be2ea 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png index 2c47fac3a..2e327dff0 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts new file mode 100644 index 000000000..154ac3c16 --- /dev/null +++ b/browser_tests/tests/lodThreshold.spec.ts @@ -0,0 +1,201 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('LOD Threshold', () => { + test('Should switch to low quality mode at correct zoom threshold', async ({ + comfyPage + }) => { + // Load a workflow with some nodes to render + await comfyPage.loadWorkflow('default') + + // Get initial LOD state and settings + const initialState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale, + minFontSize: canvas.min_font_size_for_lod + } + }) + + // Should start at normal zoom (not low quality) + expect(initialState.lowQuality).toBe(false) + expect(initialState.scale).toBeCloseTo(1, 1) + + // Calculate expected threshold (8px / 14px ≈ 0.571) + const expectedThreshold = initialState.minFontSize / 14 + // Can't access private _lowQualityZoomThreshold directly + + // Zoom out just above threshold (should still be high quality) + await comfyPage.zoom(120, 5) // Zoom out 5 steps + await comfyPage.nextFrame() + + const aboveThresholdState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + // If still above threshold, should be high quality + if (aboveThresholdState.scale > expectedThreshold) { + expect(aboveThresholdState.lowQuality).toBe(false) + } + + // Zoom out more to trigger LOD (below threshold) + await comfyPage.zoom(120, 5) // Zoom out 5 more steps + await comfyPage.nextFrame() + + // Check that LOD is now active + const zoomedOutState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + expect(zoomedOutState.scale).toBeLessThan(expectedThreshold) + expect(zoomedOutState.lowQuality).toBe(true) + + // Zoom back in to disable LOD (above threshold) + await comfyPage.zoom(-120, 15) // Zoom in 15 steps + await comfyPage.nextFrame() + + // Check that LOD is now inactive + const zoomedInState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + expect(zoomedInState.scale).toBeGreaterThan(expectedThreshold) + expect(zoomedInState.lowQuality).toBe(false) + }) + + test('Should update threshold when font size setting changes', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('default') + + // Change the font size setting to 14px (more aggressive LOD) + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 14) + + // Check that font size updated + const newState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + minFontSize: canvas.min_font_size_for_lod + } + }) + + expect(newState.minFontSize).toBe(14) + // Expected threshold would be 14px / 14px = 1.0 + + // At default zoom, LOD should still be inactive (scale is exactly 1.0, not less than) + const lodState = await comfyPage.page.evaluate(() => { + return window['app'].canvas.low_quality + }) + expect(lodState).toBe(false) + + // Zoom out slightly to trigger LOD + await comfyPage.zoom(120, 1) // Zoom out 1 step + await comfyPage.nextFrame() + + const afterZoom = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + expect(afterZoom.scale).toBeLessThan(1.0) + expect(afterZoom.lowQuality).toBe(true) + }) + + test('Should disable LOD when font size is set to 0', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('default') + + // Disable LOD by setting font size to 0 + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + + // Zoom out significantly + await comfyPage.zoom(120, 20) // Zoom out 20 steps + await comfyPage.nextFrame() + + // LOD should remain disabled even at very low zoom + const state = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale, + minFontSize: canvas.min_font_size_for_lod + } + }) + + expect(state.minFontSize).toBe(0) // LOD disabled + expect(state.lowQuality).toBe(false) + expect(state.scale).toBeLessThan(0.2) // Very zoomed out + }) + + test('Should show visual difference between LOD on and off', async ({ + comfyPage + }) => { + // Load a workflow with text-heavy nodes for clear visual difference + await comfyPage.loadWorkflow('default') + + // Set zoom level clearly below the threshold to ensure LOD activates + const targetZoom = 0.4 // Well below default threshold of ~0.571 + + // Zoom to target level + await comfyPage.page.evaluate((zoom) => { + window['app'].canvas.ds.scale = zoom + window['app'].canvas.setDirty(true, true) + }, targetZoom) + await comfyPage.nextFrame() + + // Take snapshot with LOD active (default 8px setting) + await expect(comfyPage.canvas).toHaveScreenshot( + 'lod-comparison-low-quality.png' + ) + + const lowQualityState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + expect(lowQualityState.lowQuality).toBe(true) + + // Disable LOD to see high quality at same zoom + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.nextFrame() + + // Take snapshot with LOD disabled (full quality at same zoom) + await expect(comfyPage.canvas).toHaveScreenshot( + 'lod-comparison-high-quality.png' + ) + + const highQualityState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + expect(highQualityState.lowQuality).toBe(false) + expect(highQualityState.scale).toBeCloseTo(targetZoom, 2) + }) +}) diff --git a/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-high-quality-chromium-linux.png b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-high-quality-chromium-linux.png new file mode 100644 index 000000000..0b2859d12 Binary files /dev/null and b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-high-quality-chromium-linux.png differ diff --git a/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-low-quality-chromium-linux.png b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-low-quality-chromium-linux.png new file mode 100644 index 000000000..a6d8fe852 Binary files /dev/null and b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-low-quality-chromium-linux.png differ diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts index 355acb590..fa46778a4 100644 --- a/browser_tests/tests/menu.spec.ts +++ b/browser_tests/tests/menu.spec.ts @@ -178,6 +178,72 @@ test.describe('Menu', () => { await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) expect(await comfyPage.getVisibleToastCount()).toBe(1) }) + + test('Can navigate Theme menu and switch between Dark and Light themes', async ({ + comfyPage + }) => { + const { topbar } = comfyPage.menu + + // Take initial screenshot with default theme + await comfyPage.attachScreenshot('theme-initial') + + // Open the topbar menu + const menu = await topbar.openTopbarMenu() + await expect(menu).toBeVisible() + + // Get theme menu items + const { + submenu: themeSubmenu, + darkTheme: darkThemeItem, + lightTheme: lightThemeItem + } = await topbar.getThemeMenuItems() + + await expect(darkThemeItem).toBeVisible() + await expect(lightThemeItem).toBeVisible() + + // Switch to Light theme + await topbar.switchTheme('light') + + // Verify menu stays open and Light theme shows as active + await expect(menu).toBeVisible() + await expect(themeSubmenu).toBeVisible() + + // Check that Light theme is active + expect(await topbar.isMenuItemActive(lightThemeItem)).toBe(true) + + // Screenshot with light theme active + await comfyPage.attachScreenshot('theme-menu-light-active') + + // Verify ColorPalette setting is set to "light" + expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('light') + + // Close menu to see theme change + await topbar.closeTopbarMenu() + + // Re-open menu and get theme items again + await topbar.openTopbarMenu() + const themeItems2 = await topbar.getThemeMenuItems() + + // Switch back to Dark theme + await topbar.switchTheme('dark') + + // Verify menu stays open and Dark theme shows as active + await expect(menu).toBeVisible() + await expect(themeItems2.submenu).toBeVisible() + + // Check that Dark theme is active and Light theme is not + expect(await topbar.isMenuItemActive(themeItems2.darkTheme)).toBe(true) + expect(await topbar.isMenuItemActive(themeItems2.lightTheme)).toBe(false) + + // Screenshot with dark theme active + await comfyPage.attachScreenshot('theme-menu-dark-active') + + // Verify ColorPalette setting is set to "dark" + expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('dark') + + // Close menu + await topbar.closeTopbarMenu() + }) }) // Only test 'Top' to reduce test time. diff --git a/browser_tests/tests/nodeBadge.spec.ts b/browser_tests/tests/nodeBadge.spec.ts index 984dd6ea1..111efe29c 100644 --- a/browser_tests/tests/nodeBadge.spec.ts +++ b/browser_tests/tests/nodeBadge.spec.ts @@ -4,6 +4,10 @@ import type { ComfyApp } from '../../src/scripts/app' import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Node Badge', () => { test('Can add badge', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Hide-built-in-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Hide-built-in-chromium-linux.png index 3358702a6..eaf2a528c 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Hide-built-in-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Hide-built-in-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-None-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-None-chromium-linux.png index a8c5b98d6..8b8c993c3 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-None-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-None-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Show-all-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Show-all-chromium-linux.png index a1fc9d255..97a24c6dc 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Show-all-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-Show-all-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png index ad94cd62a..3e28c594a 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png index 0079e6ce6..b81e9e785 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png index f7734e3c4..7b5a380fc 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png index 197644a0f..1c2e2e274 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png index 256eb1c9e..6975bdeae 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png differ diff --git a/browser_tests/tests/nodeDisplay.spec.ts b/browser_tests/tests/nodeDisplay.spec.ts index 2b76d4542..fdaae14bc 100644 --- a/browser_tests/tests/nodeDisplay.spec.ts +++ b/browser_tests/tests/nodeDisplay.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + // If an input is optional by node definition, it should be shown as // a hollow circle no matter what shape it was defined in the workflow JSON. test.describe('Optional input', () => { diff --git a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/default-input-chromium-linux.png b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/default-input-chromium-linux.png index e6f4f2f26..415531498 100644 Binary files a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/default-input-chromium-linux.png and b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/default-input-chromium-linux.png differ diff --git a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/dynamically-added-input-chromium-linux.png b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/dynamically-added-input-chromium-linux.png index 43c3494e5..5ae61ea03 100644 Binary files a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/dynamically-added-input-chromium-linux.png and b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/dynamically-added-input-chromium-linux.png differ diff --git a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/force-input-chromium-linux.png b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/force-input-chromium-linux.png index 8a9d768ea..f76546805 100644 Binary files a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/force-input-chromium-linux.png and b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/force-input-chromium-linux.png differ diff --git a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/missing-nodes-converted-widget-chromium-linux.png b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/missing-nodes-converted-widget-chromium-linux.png index bcff87094..84ed95b0d 100644 Binary files a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/missing-nodes-converted-widget-chromium-linux.png and b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/missing-nodes-converted-widget-chromium-linux.png differ diff --git a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/optional-input-chromium-linux.png b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/optional-input-chromium-linux.png index fde5fce12..10c5870f3 100644 Binary files a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/optional-input-chromium-linux.png and b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/optional-input-chromium-linux.png differ diff --git a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/simple-slider-chromium-linux.png b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/simple-slider-chromium-linux.png index 8f780a126..ecea34922 100644 Binary files a/browser_tests/tests/nodeDisplay.spec.ts-snapshots/simple-slider-chromium-linux.png and b/browser_tests/tests/nodeDisplay.spec.ts-snapshots/simple-slider-chromium-linux.png differ diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index 68ce7b8d5..764849286 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -46,7 +46,7 @@ test.describe('Node Help', () => { // Click the help button in the selection toolbox const helpButton = comfyPage.selectionToolbox.locator( - 'button:has(.pi-question-circle)' + 'button[data-testid="info-button"]' ) await expect(helpButton).toBeVisible() await helpButton.click() @@ -164,7 +164,7 @@ test.describe('Node Help', () => { // Click help button const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -194,7 +194,7 @@ test.describe('Node Help', () => { // Click help button const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -228,7 +228,7 @@ test.describe('Node Help', () => { await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -276,7 +276,7 @@ test.describe('Node Help', () => { await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -348,7 +348,7 @@ This is documentation for a custom node. } const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) if (await helpButton.isVisible()) { await helpButton.click() @@ -389,7 +389,7 @@ This is documentation for a custom node. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -456,7 +456,7 @@ This is English documentation. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -479,7 +479,7 @@ This is English documentation. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -522,7 +522,7 @@ This is English documentation. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -538,7 +538,7 @@ This is English documentation. // Click help button again const helpButton2 = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton2.click() diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index 3c5e3cbe2..98ba33583 100644 --- a/browser_tests/tests/nodeSearchBox.spec.ts +++ b/browser_tests/tests/nodeSearchBox.spec.ts @@ -3,6 +3,10 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Node search box', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box') diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png index 9f72d07f6..1202e34d9 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png index 0f6c071d5..f4bd4d3f9 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png index 675ac2042..0d57dcd25 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png index b87713c14..127e5be05 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png index b87713c14..127e5be05 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png index 665260edd..ade1f674d 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png differ diff --git a/browser_tests/tests/noteNode.spec.ts b/browser_tests/tests/noteNode.spec.ts index 0f3d6a317..52dc57542 100644 --- a/browser_tests/tests/noteNode.spec.ts +++ b/browser_tests/tests/noteNode.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Note Node', () => { test('Can load node nodes', async ({ comfyPage }) => { await comfyPage.loadWorkflow('nodes/note_nodes') diff --git a/browser_tests/tests/noteNode.spec.ts-snapshots/note-nodes-chromium-linux.png b/browser_tests/tests/noteNode.spec.ts-snapshots/note-nodes-chromium-linux.png index 78eb3dcab..509067e9f 100644 Binary files a/browser_tests/tests/noteNode.spec.ts-snapshots/note-nodes-chromium-linux.png and b/browser_tests/tests/noteNode.spec.ts-snapshots/note-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts b/browser_tests/tests/primitiveNode.spec.ts index 7fc408e8b..0584a3bec 100644 --- a/browser_tests/tests/primitiveNode.spec.ts +++ b/browser_tests/tests/primitiveNode.spec.ts @@ -3,6 +3,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Primitive Node', () => { test('Can load with correct size', async ({ comfyPage }) => { await comfyPage.loadWorkflow('primitive/primitive_node') diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png index 9f713b4a1..d64126266 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png index 3d0809c96..40d877ff9 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-dom-widget-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-dom-widget-chromium-linux.png index d64f859aa..ff50afcd4 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-dom-widget-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-dom-widget-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png index 588674402..b8c243f0b 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png differ diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index 3231f47ee..7a54cae07 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' -import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Remote COMBO Widget', () => { const mockOptions = ['d', 'c', 'b', 'a'] @@ -48,7 +49,8 @@ test.describe('Remote COMBO Widget', () => { const waitForWidgetUpdate = async (comfyPage: ComfyPage) => { // Force re-render to trigger first access of widget's options await comfyPage.page.mouse.click(400, 300) - await comfyPage.page.waitForTimeout(256) + // Wait for the widget to actually update instead of fixed timeout + await comfyPage.page.waitForTimeout(300) } test.beforeEach(async ({ comfyPage }) => { @@ -189,7 +191,9 @@ test.describe('Remote COMBO Widget', () => { await comfyPage.page.keyboard.press('Control+A') await expect( - comfyPage.page.locator('.selection-toolbox .pi-refresh') + comfyPage.page.locator( + '.selection-toolbox button[data-testid="refresh-button"]' + ) ).toBeVisible() }) @@ -210,9 +214,15 @@ test.describe('Remote COMBO Widget', () => { await waitForWidgetUpdate(comfyPage) const initialOptions = await getWidgetOptions(comfyPage, nodeName) - // Wait for the refresh (TTL) to expire - await comfyPage.page.waitForTimeout(512) - await comfyPage.page.mouse.click(100, 100) + // Wait for the refresh (TTL) to expire with extra buffer for processing + // TTL is 300ms, wait 600ms to ensure it has expired + await comfyPage.page.waitForTimeout(600) + + // Click on the canvas to trigger widget refresh + await comfyPage.page.mouse.click(400, 300) + + // Wait a bit for the refresh to complete + await comfyPage.page.waitForTimeout(100) const refreshedOptions = await getWidgetOptions(comfyPage, nodeName) expect(refreshedOptions).not.toEqual(initialOptions) diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 89fdf38b2..0b2b1e0f6 100644 --- a/browser_tests/tests/rerouteNode.spec.ts +++ b/browser_tests/tests/rerouteNode.spec.ts @@ -40,6 +40,7 @@ test.describe('Reroute Node', () => { test.describe('LiteGraph Native Reroute Node', () => { test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) }) diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-0-5x-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-0-5x-linux.png index f58efed43..75c7dd2b3 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-0-5x-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-0-5x-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-2x-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-2x-linux.png index d29787e49..be8c019f3 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-2x-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-2x-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png index 030471469..d7ee9ff26 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png index 2db4387ff..11ecdabd0 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png index 5cedff547..8723985d2 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-delete-from-midpoint-context-menu-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-delete-from-midpoint-context-menu-chromium-linux.png index 40fda2b55..c07494c21 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-delete-from-midpoint-context-menu-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-delete-from-midpoint-context-menu-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png index cf3fe09a1..a9e9926bf 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts b/browser_tests/tests/rightClickMenu.spec.ts index db21ecd36..f7718122b 100644 --- a/browser_tests/tests/rightClickMenu.spec.ts +++ b/browser_tests/tests/rightClickMenu.spec.ts @@ -3,6 +3,10 @@ import { expect } from '@playwright/test' import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Canvas Right Click Menu', () => { test('Can add node', async ({ comfyPage }) => { await comfyPage.rightClickCanvas() diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png index 9693f5373..d141d9912 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png index 9d1197e35..2755d74c5 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png index 76807cf4c..fbadb08da 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png index 0a9eb155c..a73ca4c33 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png index 0bb17b965..238946afa 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png index 1ca78bae4..1635e4e89 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png index 18344dd3e..4c1ed8d69 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png index cc2870711..ea0156109 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png index f930bb889..4910f554a 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-properties-panel-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-properties-panel-chromium-linux.png index 0dba66629..f381be240 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-properties-panel-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-properties-panel-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png index 088ecf100..4440ef029 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png index 1ca78bae4..1635e4e89 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png index da720706b..700e9d2c5 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png index a37ea3f8c..a6d09ff18 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png index 4512bc9da..c0b91f3ec 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png index 821129dcb..a80e1ad38 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index 90568e3aa..6b8576982 100644 --- a/browser_tests/tests/selectionToolbox.spec.ts +++ b/browser_tests/tests/selectionToolbox.spec.ts @@ -4,6 +4,9 @@ import { comfyPageFixture } from '../fixtures/ComfyPage' const test = comfyPageFixture +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) const BLUE_COLOR = 'rgb(51, 51, 85)' const RED_COLOR = 'rgb(85, 51, 51)' @@ -43,7 +46,7 @@ test.describe('Selection Toolbox', () => { const boundingBox = await toolboxContainer.boundingBox() expect(boundingBox).not.toBeNull() // Canvas-based positioning can vary, just verify toolbox appears in reasonable bounds - expect(boundingBox!.x).toBeGreaterThan(-100) // Not too far off-screen left + expect(boundingBox!.x).toBeGreaterThan(-200) // Not too far off-screen left expect(boundingBox!.x).toBeLessThan(1000) // Not too far off-screen right expect(boundingBox!.y).toBeGreaterThan(-100) // Not too far off-screen top }) @@ -149,7 +152,7 @@ test.describe('Selection Toolbox', () => { // Node should have the selected color class/style // Note: Exact verification method depends on how color is applied to nodes const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] - expect(selectedNode.getProperty('color')).not.toBeNull() + expect(await selectedNode.getProperty('color')).not.toBeNull() }) test('color picker shows current color of selected nodes', async ({ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png index 12215637f..96f6507e1 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png index a2d11d350..af92221f3 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png index 93924ff73..f9b9b012c 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png index 8017b8f49..2006231c7 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts new file mode 100644 index 000000000..db6326152 --- /dev/null +++ b/browser_tests/tests/selectionToolboxSubmenus.spec.ts @@ -0,0 +1,181 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Selection Toolbox - More Options Submenus', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.nextFrame() + await comfyPage.selectNodes(['KSampler']) + await comfyPage.nextFrame() + }) + + const openMoreOptions = async (comfyPage: any) => { + const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') + if (ksamplerNodes.length === 0) { + throw new Error('No KSampler nodes found') + } + + // Drag the KSampler to the center of the screen + const nodePos = await ksamplerNodes[0].getPosition() + const viewportSize = comfyPage.page.viewportSize() + const centerX = viewportSize.width / 3 + const centerY = viewportSize.height / 2 + await comfyPage.dragAndDrop( + { x: nodePos.x, y: nodePos.y }, + { x: centerX, y: centerY } + ) + await comfyPage.nextFrame() + + await ksamplerNodes[0].click('title') + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(500) + + await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({ + timeout: 5000 + }) + + const moreOptionsBtn = comfyPage.page.locator( + '[data-testid="more-options-button"]' + ) + await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 }) + + await comfyPage.page.click('[data-testid="more-options-button"]') + + await comfyPage.nextFrame() + + const menuOptionsVisible = await comfyPage.page + .getByText('Rename') + .isVisible({ timeout: 2000 }) + .catch(() => false) + if (menuOptionsVisible) { + return + } + + await moreOptionsBtn.click({ force: true }) + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(2000) + + const menuOptionsVisibleAfterClick = await comfyPage.page + .getByText('Rename') + .isVisible({ timeout: 2000 }) + .catch(() => false) + if (menuOptionsVisibleAfterClick) { + return + } + + throw new Error('Could not open More Options menu - popover not showing') + } + + test('opens Node Info from More Options menu', async ({ comfyPage }) => { + await openMoreOptions(comfyPage) + const nodeInfoButton = comfyPage.page.getByText('Node Info', { + exact: true + }) + await expect(nodeInfoButton).toBeVisible() + await nodeInfoButton.click() + await comfyPage.nextFrame() + }) + + test('changes node shape via Shape submenu', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const initialShape = await nodeRef.getProperty('shape') + + await openMoreOptions(comfyPage) + await comfyPage.page.getByText('Shape', { exact: true }).click() + await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({ + timeout: 5000 + }) + await comfyPage.page.getByText('Box', { exact: true }).click() + await comfyPage.nextFrame() + + const newShape = await nodeRef.getProperty('shape') + expect(newShape).not.toBe(initialShape) + expect(newShape).toBe(1) + }) + + test('changes node color via Color submenu swatch', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const initialColor = await nodeRef.getProperty('color') + + await openMoreOptions(comfyPage) + await comfyPage.page.getByText('Color', { exact: true }).click() + const blueSwatch = comfyPage.page.locator('[title="Blue"]') + await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 }) + await blueSwatch.first().click() + await comfyPage.nextFrame() + + const newColor = await nodeRef.getProperty('color') + expect(newColor).toBe('#223') + if (initialColor) { + expect(newColor).not.toBe(initialColor) + } + }) + + test('renames a node using Rename action', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + await openMoreOptions(comfyPage) + await comfyPage.page + .getByText('Rename', { exact: true }) + .click({ force: true }) + const input = comfyPage.page.locator( + '.group-title-editor.node-title-editor .editable-text input' + ) + await expect(input).toBeVisible() + await input.fill('RenamedNode') + await input.press('Enter') + await comfyPage.nextFrame() + const newTitle = await nodeRef.getProperty('title') + expect(newTitle).toBe('RenamedNode') + }) + + test('closes More Options menu when clicking outside', async ({ + comfyPage + }) => { + await openMoreOptions(comfyPage) + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).toBeVisible({ timeout: 5000 }) + + await comfyPage.page + .locator('#graph-canvas') + .click({ position: { x: 0, y: 50 }, force: true }) + await comfyPage.nextFrame() + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).not.toBeVisible() + }) + + test('closes More Options menu when clicking the button again (toggle)', async ({ + comfyPage + }) => { + await openMoreOptions(comfyPage) + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).toBeVisible({ timeout: 5000 }) + + await comfyPage.page.evaluate(() => { + const btn = document.querySelector('[data-testid="more-options-button"]') + if (btn) { + const event = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + detail: 1 + }) + btn.dispatchEvent(event) + } + }) + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(500) + + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).not.toBeVisible() + }) +}) diff --git a/browser_tests/tests/sidebar/queue.spec.ts b/browser_tests/tests/sidebar/queue.spec.ts index 2d9dd10ba..39e2ced6e 100644 --- a/browser_tests/tests/sidebar/queue.spec.ts +++ b/browser_tests/tests/sidebar/queue.spec.ts @@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => { comfyPage }) => { await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(firstImage) + ).toBeVisible() }) test('maintains active gallery item when new tasks are added', async ({ @@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => { const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage) await newTask.waitFor({ state: 'visible' }) // The active gallery item should still be the initial image - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(firstImage) + ).toBeVisible() }) test.describe('Gallery navigation', () => { @@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => { delay: 256 }) await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(end) + ).toBeVisible() }) }) }) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index f2c2e2bb5..625233213 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -1,4 +1,5 @@ -import { Page, expect } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' @@ -79,6 +80,12 @@ test.describe('Templates', () => { // Load a template await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() + + await comfyPage.page + .locator( + 'nav > div:nth-child(2) > div > span:has-text("Getting Started")' + ) + .click() await comfyPage.templates.loadTemplate('default') await expect(comfyPage.templates.content).toBeHidden() @@ -101,48 +108,72 @@ test.describe('Templates', () => { expect(await comfyPage.templates.content.isVisible()).toBe(true) }) - test('Uses title field as fallback when the key is not found in locales', async ({ + test('Uses proper locale files for templates', async ({ comfyPage }) => { + // Set locale to French before opening templates + await comfyPage.setSetting('Comfy.Locale', 'fr') + + // Load the templates dialog and wait for the French index file request + const requestPromise = comfyPage.page.waitForRequest( + '**/templates/index.fr.json' + ) + + await comfyPage.executeCommand('Comfy.BrowseTemplates') + + const request = await requestPromise + + // Verify French index was requested + expect(request.url()).toContain('templates/index.fr.json') + + await expect(comfyPage.templates.content).toBeVisible() + }) + + test('Falls back to English templates when locale file not found', async ({ comfyPage }) => { - // Capture request for the index.json - await comfyPage.page.route('**/templates/index.json', async (route, _) => { - // Add a new template that won't have a translation pre-generated - const response = [ - { - moduleName: 'default', - title: 'FALLBACK CATEGORY', - type: 'image', - templates: [ - { - name: 'unknown_key_has_no_translation_available', - title: 'FALLBACK TEMPLATE NAME', - mediaType: 'image', - mediaSubtype: 'webp', - description: 'No translations found' - } - ] - } - ] + // Set locale to a language that doesn't have a template file + await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists + + // Wait for the German request (expected to 404) + const germanRequestPromise = comfyPage.page.waitForRequest( + '**/templates/index.de.json' + ) + + // Wait for the fallback English request + const englishRequestPromise = comfyPage.page.waitForRequest( + '**/templates/index.json' + ) + + // Intercept the German file to simulate a 404 + await comfyPage.page.route('**/templates/index.de.json', async (route) => { await route.fulfill({ - status: 200, - body: JSON.stringify(response), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } + status: 404, + headers: { 'Content-Type': 'text/plain' }, + body: 'Not Found' }) }) + // Allow the English index to load normally + await comfyPage.page.route('**/templates/index.json', (route) => + route.continue() + ) + // Load the templates dialog await comfyPage.executeCommand('Comfy.BrowseTemplates') + await expect(comfyPage.templates.content).toBeVisible() - // Expect the title to be used as fallback for template cards + // Verify German was requested first, then English as fallback + const germanRequest = await germanRequestPromise + const englishRequest = await englishRequestPromise + + expect(germanRequest.url()).toContain('templates/index.de.json') + expect(englishRequest.url()).toContain('templates/index.json') + + // Verify English titles are shown as fallback await expect( - comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME') + comfyPage.templates.content.getByRole('heading', { + name: 'Image Generation' + }) ).toBeVisible() - - // Expect the title to be used as fallback for the template categories - await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible() }) test('template cards are dynamically sized and responsive', async ({ @@ -150,46 +181,33 @@ test.describe('Templates', () => { }) => { // Open templates dialog await comfyPage.executeCommand('Comfy.BrowseTemplates') - await expect(comfyPage.templates.content).toBeVisible() + await comfyPage.templates.content.waitFor({ state: 'visible' }) - // Wait for at least one template card to appear - await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({ - timeout: 5000 - }) + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) + const nav = comfyPage.page + .locator('header') + .filter({ hasText: 'Templates' }) - // Take snapshot of the template grid - const templateGrid = comfyPage.templates.content.locator('.grid').first() + const cardCount = await comfyPage.page + .locator('[data-testid^="template-workflow-"]') + .count() + expect(cardCount).toBeGreaterThan(0) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png') + await expect(nav).toBeVisible() // Nav should be visible at desktop size - // Check cards at mobile viewport size - await comfyPage.page.setViewportSize({ width: 640, height: 800 }) + const mobileSize = { width: 640, height: 800 } + await comfyPage.page.setViewportSize(mobileSize) + expect(cardCount).toBeGreaterThan(0) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png') + await expect(nav).not.toBeVisible() // Nav should collapse at mobile size - // Check cards at tablet size - await comfyPage.page.setViewportSize({ width: 1024, height: 800 }) + const tabletSize = { width: 1024, height: 800 } + await comfyPage.page.setViewportSize(tabletSize) + expect(cardCount).toBeGreaterThan(0) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png') - }) - - test('hover effects work on template cards', async ({ comfyPage }) => { - // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') - await expect(comfyPage.templates.content).toBeVisible() - - // Get a template card - const firstCard = comfyPage.page.locator('.template-card').first() - await expect(firstCard).toBeVisible({ timeout: 5000 }) - - // Take snapshot before hover - await expect(firstCard).toHaveScreenshot('template-card-before-hover.png') - - // Hover over the card - await firstCard.hover() - - // Take snapshot after hover to verify hover effect - await expect(firstCard).toHaveScreenshot('template-card-after-hover.png') + await expect(nav).toBeVisible() // Nav should be visible at tablet size }) test('template cards descriptions adjust height dynamically', async ({ @@ -256,21 +274,42 @@ test.describe('Templates', () => { await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() - // Verify cards are visible with varying content lengths + // Wait for cards to load await expect( - comfyPage.page.getByText('This is a short description.') - ).toBeVisible({ timeout: 5000 }) - await expect( - comfyPage.page.getByText('This is a medium length description') - ).toBeVisible({ timeout: 5000 }) - await expect( - comfyPage.page.getByText('This is a much longer description') + comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) ).toBeVisible({ timeout: 5000 }) - // Take snapshot of a grid with specific cards - const templateGrid = comfyPage.templates.content - .locator('.grid:has-text("Short Description")') - .first() + // Verify all three cards with different descriptions are visible + const shortDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) + const mediumDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-medium-description"]' + ) + const longDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-long-description"]' + ) + + await expect(shortDescCard).toBeVisible() + await expect(mediumDescCard).toBeVisible() + await expect(longDescCard).toBeVisible() + + // Verify descriptions are visible and have line-clamp class + // The description is in a p tag with text-muted class + const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2') + const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2') + const longDesc = longDescCard.locator('p.text-muted.line-clamp-2') + + await expect(shortDesc).toContainText('short description') + await expect(mediumDesc).toContainText('medium length description') + await expect(longDesc).toContainText('much longer description') + + // Verify grid layout maintains consistency + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) await expect(templateGrid).toBeVisible() await expect(templateGrid).toHaveScreenshot( 'template-grid-varying-content.png' diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png index 5e105418c..137cb4d8c 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png index 4bafa8784..080855e15 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png index ff6a6c017..0c34b0607 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png index b4b005838..54f14ea0d 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png index 8871c8eb6..a5cf133cb 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png index cabfe9a20..2548e66ae 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index 69a40ced9..a817616f8 100644 --- a/browser_tests/tests/useSettingSearch.spec.ts +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Settings Search functionality', () => { test.beforeEach(async ({ comfyPage }) => { // Register test settings to verify hidden/deprecated filtering diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts index d85f18723..4b3ff3e30 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { SystemStats } from '../../src/schemas/apiSchema' +import type { SystemStats } from '../../src/schemas/apiSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Version Mismatch Warnings', () => { @@ -85,6 +85,7 @@ test.describe('Version Mismatch Warnings', () => { test('should persist dismissed state across sessions', async ({ comfyPage }) => { + test.setTimeout(30_000) // Mock system_stats route to indicate that the installed version is always ahead of the required version await comfyPage.page.route('**/system_stats**', async (route) => { await route.fulfill({ @@ -106,6 +107,11 @@ test.describe('Version Mismatch Warnings', () => { const dismissButton = warningToast.getByRole('button', { name: 'Close' }) await dismissButton.click() + // Wait for the dismissed state to be persisted + await comfyPage.page.waitForFunction( + () => !!localStorage.getItem('comfy.versionMismatch.dismissals') + ) + // Reload the page, keeping local storage await comfyPage.setup({ clearStorage: false }) diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts b/browser_tests/tests/vueNodes/groups/groups.spec.ts new file mode 100644 index 000000000..a43e96a5d --- /dev/null +++ b/browser_tests/tests/vueNodes/groups/groups.spec.ts @@ -0,0 +1,33 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const CREATE_GROUP_HOTKEY = 'Control+g' + +test.describe('Vue Node Groups', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow creating groups with hotkey', async ({ comfyPage }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-groups-create-group.png' + ) + }) + + test('should allow fitting group to contents', async ({ comfyPage }) => { + await comfyPage.setup() + await comfyPage.loadWorkflow('groups/oversized_group') + await comfyPage.ctrlA() + await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-groups-fit-to-contents.png' + ) + }) +}) diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png new file mode 100644 index 000000000..446a50c99 Binary files /dev/null and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png new file mode 100644 index 000000000..0302d8a8f Binary files /dev/null and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts new file mode 100644 index 000000000..a91a9b928 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts @@ -0,0 +1,18 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.describe('Vue Nodes Canvas Pan', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('@mobile Can pan with touch', async ({ comfyPage }) => { + await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-nodes-paned-with-touch.png' + ) + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png new file mode 100644 index 000000000..d0a0c000d Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts new file mode 100644 index 000000000..b87309f10 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts @@ -0,0 +1,33 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.describe('Vue Nodes Zoom', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should not capture drag while zooming with ctrl+shift+drag', async ({ + comfyPage + }) => { + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const nodeBoundingBox = await checkpointNode.boundingBox() + if (!nodeBoundingBox) throw new Error('Node bounding box not available') + + const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2 + const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2 + + // Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over + // the node. The node should not capture the drag while drag-zooming. + await comfyPage.page.keyboard.down('Control') + await comfyPage.page.keyboard.down('Shift') + await comfyPage.dragAndDrop( + { x: 200, y: 300 }, + { x: nodeMidpointX, y: nodeMidpointY } + ) + + await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png') + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png new file mode 100644 index 000000000..1f47aab5c Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts new file mode 100644 index 000000000..7c0cd4c1d --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -0,0 +1,221 @@ +import type { Locator } from '@playwright/test' + +import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier' +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' +import { fitToViewInstant } from '../../../../helpers/fitToView' + +async function getCenter(locator: Locator): Promise<{ x: number; y: number }> { + const box = await locator.boundingBox() + if (!box) throw new Error('Slot bounding box not available') + return { + x: box.x + box.width / 2, + y: box.y + box.height / 2 + } +} + +test.describe('Vue Node Link Interaction', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.vueNodes.waitForNodes() + await fitToViewInstant(comfyPage) + }) + + test('should show a link dragging out from a slot when dragging on a slot', async ({ + comfyPage, + comfyMouse + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + + const samplerNode = samplerNodes[0] + const outputSlot = await samplerNode.getOutput(0) + await outputSlot.removeLinks() + await comfyPage.nextFrame() + + const slotKey = getSlotKey(String(samplerNode.id), 0, false) + const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`) + await expect(slotLocator).toBeVisible() + + const start = await getCenter(slotLocator) + const canvasBox = await comfyPage.canvas.boundingBox() + if (!canvasBox) throw new Error('Canvas bounding box not available') + + // Arbitrary value + const dragTarget = { + x: start.x + 180, + y: start.y - 140 + } + + await comfyMouse.move(start) + await comfyMouse.drag(dragTarget) + await comfyPage.nextFrame() + + try { + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-dragging-link.png' + ) + } finally { + await comfyMouse.drop() + } + }) + + test('should create a link when dropping on a compatible slot', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + expect(vaeNodes.length).toBeGreaterThan(0) + const vaeNode = vaeNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const vaeInput = await vaeNode.getInput(0) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(1) + expect(await vaeInput.getLinkCount()).toBe(1) + + const linkDetails = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph + if (!graph) return null + + const source = graph.getNodeById(sourceId) + if (!source) return null + + const linkId = source.outputs[0]?.links?.[0] + if (linkId == null) return null + + const link = graph.links[linkId] + if (!link) return null + + return { + originId: link.origin_id, + originSlot: link.origin_slot, + targetId: link.target_id, + targetSlot: link.target_slot + } + }, samplerNode.id) + + expect(linkDetails).not.toBeNull() + expect(linkDetails).toMatchObject({ + originId: samplerNode.id, + originSlot: 0, + targetId: vaeNode.id, + targetSlot: 0 + }) + }) + + test('should not create a link when slot types are incompatible', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + expect(clipNodes.length).toBeGreaterThan(0) + const clipNode = clipNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const clipInput = await clipNode.getInput(0) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(clipNode.id), 0, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(0) + expect(await clipInput.getLinkCount()).toBe(0) + + const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph + if (!graph) return 0 + + const source = graph.getNodeById(sourceId) + if (!source) return 0 + + return source.outputs[0]?.links?.length ?? 0 + }, samplerNode.id) + + expect(graphLinkCount).toBe(0) + }) + + test('should not create a link when dropping onto a slot on the same node', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const samplerInput = await samplerNode.getInput(3) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(0) + expect(await samplerInput.getLinkCount()).toBe(0) + + const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph + if (!graph) return 0 + + const source = graph.getNodeById(sourceId) + if (!source) return 0 + + return source.outputs[0]?.links?.length ?? 0 + }, samplerNode.id) + + expect(graphLinkCount).toBe(0) + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png new file mode 100644 index 000000000..a30e4b2c0 Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts b/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts new file mode 100644 index 000000000..e7a610643 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts @@ -0,0 +1,145 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Vue Nodes - Delete Key Interaction', () => { + test.beforeEach(async ({ comfyPage }) => { + // Enable Vue nodes rendering + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.setup() + }) + + test('Can select all and delete Vue nodes with Delete key', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + + // Get initial Vue node count + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(initialNodeCount).toBeGreaterThan(0) + + // Select all Vue nodes + await comfyPage.ctrlA() + + // Verify all Vue nodes are selected + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(initialNodeCount) + + // Delete with Delete key + await comfyPage.vueNodes.deleteSelected() + + // Verify all Vue nodes were deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(0) + }) + + test('Can select specific Vue node and delete it', async ({ comfyPage }) => { + await comfyPage.vueNodes.waitForNodes() + + // Get initial Vue node count + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(initialNodeCount).toBeGreaterThan(0) + + // Get first Vue node ID and select it + const nodeIds = await comfyPage.vueNodes.getNodeIds() + await comfyPage.vueNodes.selectNode(nodeIds[0]) + + // Verify selection + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(1) + + // Delete with Delete key + await comfyPage.vueNodes.deleteSelected() + + // Verify one Vue node was deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(initialNodeCount - 1) + }) + + test('Can select and delete Vue node with Backspace key', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + + // Select first Vue node + const nodeIds = await comfyPage.vueNodes.getNodeIds() + await comfyPage.vueNodes.selectNode(nodeIds[0]) + + // Delete with Backspace key instead of Delete + await comfyPage.vueNodes.deleteSelectedWithBackspace() + + // Verify Vue node was deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(initialNodeCount - 1) + }) + + test('Delete key does not delete node when typing in Vue node widgets', async ({ + comfyPage + }) => { + const initialNodeCount = await comfyPage.getGraphNodesCount() + + // Find a text input widget in a Vue node + const textWidget = comfyPage.page + .locator('input[type="text"], textarea') + .first() + + // Click on text widget to focus it + await textWidget.click() + await textWidget.fill('test text') + + // Press Delete while focused on widget - should delete text, not node + await textWidget.press('Delete') + + // Node count should remain the same + const finalNodeCount = await comfyPage.getGraphNodesCount() + expect(finalNodeCount).toBe(initialNodeCount) + }) + + test('Delete key does not delete node when nothing is selected', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + + // Ensure no Vue nodes are selected + await comfyPage.vueNodes.clearSelection() + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(0) + + // Press Delete key - should not crash and should handle gracefully + await comfyPage.page.keyboard.press('Delete') + + // Vue node count should remain the same + const nodeCount = await comfyPage.vueNodes.getNodeCount() + expect(nodeCount).toBeGreaterThan(0) + }) + + test('Can multi-select with Ctrl+click and delete multiple Vue nodes', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + + // Multi-select first two Vue nodes using Ctrl+click + const nodeIds = await comfyPage.vueNodes.getNodeIds() + const nodesToSelect = nodeIds.slice(0, 2) + await comfyPage.vueNodes.selectNodes(nodesToSelect) + + // Verify expected nodes are selected + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(nodesToSelect.length) + + // Delete selected Vue nodes + await comfyPage.vueNodes.deleteSelected() + + // Verify expected nodes were deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(initialNodeCount - nodesToSelect.length) + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts new file mode 100644 index 000000000..3984989e1 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts @@ -0,0 +1,76 @@ +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() + }) + + test('should display node title', async ({ comfyPage }) => { + // 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 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') + 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 + const titleElement = await vueNode.getTitleElement() + await titleElement.dblclick() + await comfyPage.nextFrame() + + // 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 + 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') + const nodeBbox = await loadCheckpointNode.boundingBox() + if (!nodeBbox) throw new Error('Node not found') + await loadCheckpointNode.dblclick() + + const editingTitleInput = comfyPage.page.getByTestId('node-title-input') + await expect(editingTitleInput).not.toBeVisible() + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts new file mode 100644 index 000000000..2af676589 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts @@ -0,0 +1,52 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Vue Node Selection', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + const modifiers = [ + { key: 'Control', name: 'ctrl' }, + { key: 'Shift', name: 'shift' }, + { key: 'Meta', name: 'meta' } + ] as const + + for (const { key: modifier, name } of modifiers) { + test(`should allow selecting multiple nodes with ${name}+click`, async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + await comfyPage.page.getByText('Empty Latent Image').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2) + + await comfyPage.page.getByText('KSampler').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3) + }) + + test(`should allow de-selecting nodes with ${name}+click`, async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + await comfyPage.page.getByText('Load Checkpoint').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0) + }) + } +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts new file mode 100644 index 000000000..74ec17cc9 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -0,0 +1,45 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const BYPASS_HOTKEY = 'Control+b' +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.vueNodes.waitForNodes() + }) + + test('should allow toggling bypass on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + }) + + test('should allow toggling bypass on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + await expect(ksamplerNode).toHaveClass(BYPASS_CLASS) + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts new file mode 100644 index 000000000..a339a0a25 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts @@ -0,0 +1,89 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' +import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures' + +test.describe('Vue Node Collapse', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.setSetting('Comfy.EnableTooltips', true) + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + }) + + test('should allow collapsing node with collapse icon', async ({ + comfyPage + }) => { + // Get the KSampler node from the default workflow + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Initially should not be collapsed + expect(await node.isCollapsed()).toBe(false) + const body = await vueNode.getBody() + await expect(body).toBeVisible() + + // Collapse the node + await vueNode.toggleCollapse() + expect(await node.isCollapsed()).toBe(true) + + // Verify node content is hidden + const collapsedSize = await node.getSize() + await expect(body).not.toBeVisible() + + // Expand again + await vueNode.toggleCollapse() + expect(await node.isCollapsed()).toBe(false) + await expect(body).toBeVisible() + + // Size should be restored + const expandedSize = await node.getSize() + expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height) + }) + + test('should show collapse/expand icon state', async ({ comfyPage }) => { + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Check initial expanded state icon + let iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-down') + + // Collapse and check icon + await vueNode.toggleCollapse() + iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-right') + + // Expand and check icon + await vueNode.toggleCollapse() + iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-down') + }) + + test('should preserve title when collapsing/expanding', async ({ + comfyPage + }) => { + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Set custom title + await vueNode.setTitle('Test Sampler') + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Collapse + await vueNode.toggleCollapse() + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Expand + await vueNode.toggleCollapse() + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Verify title is still displayed + const header = await vueNode.getHeader() + await expect(header).toContainText('Test Sampler') + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts new file mode 100644 index 000000000..e61e3ca01 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts @@ -0,0 +1,49 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.describe('Vue Node Custom Colors', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('displays color picker button and allows color selection', async ({ + comfyPage + }) => { + const loadCheckpointNode = comfyPage.page.locator('[data-node-id]').filter({ + hasText: 'Load Checkpoint' + }) + await loadCheckpointNode.getByText('Load Checkpoint').click() + + await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() + await comfyPage.page + .locator('.color-picker-container') + .locator('i[data-testid="blue"]') + .click() + + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-custom-color-blue.png' + ) + }) + + test('should load node colors from workflow', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('nodes/every_node_color') + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-custom-colors-dark-all-colors.png' + ) + }) + + test('should show brightened node colors on light theme', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.loadWorkflow('nodes/every_node_color') + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-custom-colors-light-all-colors.png' + ) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png new file mode 100644 index 000000000..64898a216 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png new file mode 100644 index 000000000..ac6a841ae Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png new file mode 100644 index 000000000..c89b8b240 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts new file mode 100644 index 000000000..f4f8e10fe --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts @@ -0,0 +1,32 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const ERROR_CLASS = /border-error/ + +test.describe('Vue Node Error', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should display error state when node is missing (node from workflow is not installed)', async ({ + comfyPage + }) => { + await comfyPage.setup() + await comfyPage.loadWorkflow('missing/missing_nodes') + + // Close missing nodes warning dialog + await comfyPage.page.getByRole('button', { name: 'Close' }).click() + await comfyPage.page.waitForSelector('.comfy-missing-nodes', { + state: 'hidden' + }) + + // Expect error state on missing unknown node + const unknownNode = comfyPage.page.locator('[data-node-id]').filter({ + hasText: 'UNKNOWN NODE' + }) + await expect(unknownNode).toHaveClass(ERROR_CLASS) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts new file mode 100644 index 000000000..f8c94aba5 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Vue Nodes - LOD', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + await comfyPage.loadWorkflow('default') + }) + + test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => { + await comfyPage.vueNodes.waitForNodes() + + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(initialNodeCount).toBeGreaterThan(0) + + await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png') + + const vueNodesContainer = comfyPage.vueNodes.nodes + const textboxesInNodes = vueNodesContainer.getByRole('textbox') + const buttonsInNodes = vueNodesContainer.getByRole('button') + + await expect(textboxesInNodes.first()).toBeVisible() + await expect(buttonsInNodes.first()).toBeVisible() + + await comfyPage.zoom(120, 10) + await comfyPage.nextFrame() + + await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png') + + await expect(textboxesInNodes.first()).toBeHidden() + await expect(buttonsInNodes.first()).toBeHidden() + + await comfyPage.zoom(-120, 10) + await comfyPage.nextFrame() + + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-nodes-lod-inactive.png' + ) + await expect(textboxesInNodes.first()).toBeVisible() + await expect(buttonsInNodes.first()).toBeVisible() + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png new file mode 100644 index 000000000..cf8384a03 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png new file mode 100644 index 000000000..ee051f5df Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png new file mode 100644 index 000000000..cf8384a03 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts new file mode 100644 index 000000000..37dcfd37b --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -0,0 +1,45 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const MUTE_HOTKEY = 'Control+m' +const MUTE_CLASS = /opacity-50/ + +test.describe('Vue Node Mute', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling mute on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await expect(checkpointNode).toHaveClass(MUTE_CLASS) + + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) + }) + + test('should allow toggling mute on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).toHaveClass(MUTE_CLASS) + await expect(ksamplerNode).toHaveClass(MUTE_CLASS) + + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) + await expect(ksamplerNode).not.toHaveClass(MUTE_CLASS) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts new file mode 100644 index 000000000..27f1ad1ac --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts @@ -0,0 +1,85 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const PIN_HOTKEY = 'p' +const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' + +test.describe('Vue Node Pin', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling pin on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').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() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + await expect(pinIndicator).not.toBeVisible() + }) + + test('should allow toggling pin on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + const pinIndicator1 = checkpointNode.locator(PIN_INDICATOR) + await expect(pinIndicator1).toBeVisible() + const pinIndicator2 = ksamplerNode.locator(PIN_INDICATOR) + await expect(pinIndicator2).toBeVisible() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + await expect(pinIndicator1).not.toBeVisible() + await expect(pinIndicator2).not.toBeVisible() + }) + + test('should not allow dragging pinned nodes', async ({ comfyPage }) => { + const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint') + await checkpointNodeHeader.click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + // Try to drag the node + const headerPos = await checkpointNodeHeader.boundingBox() + if (!headerPos) throw new Error('Failed to get header position') + await comfyPage.dragAndDrop( + { x: headerPos.x, y: headerPos.y }, + { x: headerPos.x + 256, y: headerPos.y + 256 } + ) + + // Verify the node is not dragged (same position before and after click-and-drag) + const headerPosAfterDrag = await checkpointNodeHeader.boundingBox() + if (!headerPosAfterDrag) + throw new Error('Failed to get header position after drag') + expect(headerPosAfterDrag).toEqual(headerPos) + + // Unpin the node with the hotkey + await checkpointNodeHeader.click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + // Try to drag the node again + await comfyPage.dragAndDrop( + { x: headerPos.x, y: headerPos.y }, + { x: headerPos.x + 256, y: headerPos.y + 256 } + ) + + // Verify the node is dragged + const headerPosAfterDrag2 = await checkpointNodeHeader.boundingBox() + if (!headerPosAfterDrag2) + throw new Error('Failed to get header position after drag') + expect(headerPosAfterDrag2).not.toEqual(headerPos) + }) +}) diff --git a/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts new file mode 100644 index 000000000..bb08232a2 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts @@ -0,0 +1,49 @@ +import { + type ComfyPage, + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.describe('Vue Multiline String Widget', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + const getFirstClipNode = (comfyPage: ComfyPage) => + comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode (Prompt)').first() + + const getFirstMultilineStringWidget = (comfyPage: ComfyPage) => + getFirstClipNode(comfyPage).getByRole('textbox', { name: 'text' }) + + test('should allow entering text', async ({ comfyPage }) => { + const textarea = getFirstMultilineStringWidget(comfyPage) + await textarea.fill('Hello World') + await expect(textarea).toHaveValue('Hello World') + await textarea.fill('Hello World 2') + await expect(textarea).toHaveValue('Hello World 2') + }) + + test('should support entering multiline content', async ({ comfyPage }) => { + const textarea = getFirstMultilineStringWidget(comfyPage) + + const multilineValue = ['Line 1', 'Line 2', 'Line 3'].join('\n') + + await textarea.fill(multilineValue) + await expect(textarea).toHaveValue(multilineValue) + }) + + test('should retain value after focus changes', async ({ comfyPage }) => { + const textarea = getFirstMultilineStringWidget(comfyPage) + + await textarea.fill('Keep me around') + + // Click another node + const loadCheckpointNode = + comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await loadCheckpointNode.click() + await getFirstClipNode(comfyPage).click() + + await expect(textarea).toHaveValue('Keep me around') + }) +}) diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index b23faabfc..3b9c05784 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Combo text widget', () => { test('Truncates text when resized', async ({ comfyPage }) => { await comfyPage.resizeLoadCheckpointNode(0.2, 1) @@ -264,7 +268,13 @@ test.describe('Animated image widget', () => { expect(filename).toContain('animated_webp.webp') }) - test('Can preview saved animated webp image', async ({ comfyPage }) => { + // FIXME: This test keeps flip-flopping because it relies on animated webp timing, + // which is inherently unreliable in CI environments. The test asset is an animated + // webp with 2 frames, and the test depends on animation frame timing to verify that + // animated webp images are properly displayed (as opposed to being treated as static webp). + // While the underlying functionality works (animated webp are correctly distinguished + // from static webp), the test is flaky due to timing dependencies with webp animation frames. + test.fixme('Can preview saved animated webp image', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/save_animated_webp') // Get position of the load animated webp node @@ -312,6 +322,9 @@ test.describe('Animated image widget', () => { test.describe('Load audio widget', () => { test('Can load audio', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/load_audio_widget') + // Wait for the audio widget to be rendered in the DOM + await comfyPage.page.waitForSelector('.comfy-audio', { state: 'attached' }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png') }) }) diff --git a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png deleted file mode 100644 index 4b948f017..000000000 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png and /dev/null differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-chromium-linux.png index dd658be51..05764b532 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-toggled-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-toggled-chromium-linux.png index e32c9a6b3..fb055e8c8 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-toggled-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/boolean-widget-toggled-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png index d0d2be94b..745e69148 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png index cc263a78b..0b7cb495d 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-drag-and-dropped-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-drag-and-dropped-chromium-linux.png index cc263a78b..0b7cb495d 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-drag-and-dropped-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-drag-and-dropped-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/ksampler-widget-added-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/ksampler-widget-added-chromium-linux.png index 71a944375..1b01d4972 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/ksampler-widget-added-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/ksampler-widget-added-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/load-audio-widget-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/load-audio-widget-chromium-linux.png index b6f7e3003..fde5a2de9 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/load-audio-widget-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/load-audio-widget-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png index 5b3f107e4..538e0b460 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png index 5aa41778b..b631cd32d 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png index 37d69e0e5..d5e10e061 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/seed-widget-dragged-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/seed-widget-dragged-chromium-linux.png index 53092167b..b356214e8 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/seed-widget-dragged-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/seed-widget-dragged-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/slider-widget-dragged-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/slider-widget-dragged-chromium-linux.png index eea983b11..2c82d9c8d 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/slider-widget-dragged-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/slider-widget-dragged-chromium-linux.png differ diff --git a/browser_tests/tsconfig.json b/browser_tests/tsconfig.json new file mode 100644 index 000000000..391298333 --- /dev/null +++ b/browser_tests/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* Test files should not be compiled */ + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts", + ] +} diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 3f795a219..5b7f4bec4 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -1,5 +1,5 @@ import path from 'path' -import { Plugin } from 'vite' +import type { Plugin } from 'vite' interface ShimResult { code: string diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts index 80ccb6c9f..bbbf14c2c 100644 --- a/build/plugins/generateImportMapPlugin.ts +++ b/build/plugins/generateImportMapPlugin.ts @@ -1,7 +1,7 @@ import glob from 'fast-glob' import fs from 'fs-extra' import { dirname, join } from 'node:path' -import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite' +import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite' interface ImportMapSource { name: string diff --git a/build/tsconfig.json b/build/tsconfig.json new file mode 100644 index 000000000..1c24810a8 --- /dev/null +++ b/build/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* Build scripts configuration */ + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 000000000..5526f900d --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "new-york", + "typescript": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/assets/css/style.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "composables": "@/composables", + "utils": "@/utils", + "ui": "@/components/ui", + "lib": "@/lib" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md new file mode 100644 index 000000000..cb4a9490f --- /dev/null +++ b/docs/SETTINGS.md @@ -0,0 +1,293 @@ +# Settings System + +## Overview + +ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration. + +### Settings Architecture +- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts` +- Registered at app startup, loaded/saved via `useSettingStore` (Pinia) +- Persisted per user via backend `/settings` endpoint +- If a value hasn't been set by the user, the store returns the computed default + +```typescript +// From src/stores/settingStore.ts:105-122 +function getDefaultValue( + key: K +): Settings[K] | undefined { + const param = getSettingById(key) + if (param === undefined) return + + const versionedDefault = getVersionedDefaultValue(key, param) + if (versionedDefault) { + return versionedDefault + } + + return typeof param.defaultValue === 'function' + ? param.defaultValue() + : param.defaultValue +} +``` + +### Settings Registration Process + +Settings are registered after server values are loaded: + +```typescript +// From src/components/graph/GraphCanvas.vue:311-315 +CORE_SETTINGS.forEach((setting) => { + settingStore.addSetting(setting) +}) + +await newUserService().initializeIfNewUser(settingStore) +``` + +## Dynamic and Environment-Based Defaults + +### Computed Defaults +You can compute defaults dynamically using function defaults that access runtime context: + +```typescript +// From src/constants/coreSettings.ts:94-101 +{ + id: 'Comfy.Sidebar.Size', + // Default to small if the window is less than 1536px(2xl) wide + defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal') +} +``` + +```typescript +// From src/constants/coreSettings.ts:306 +{ + id: 'Comfy.Locale', + defaultValue: () => navigator.language.split('-')[0] || 'en' +} +``` + +### Version-Based Defaults +You can vary defaults by installed frontend version using `defaultsByInstallVersion`: + +```typescript +// From src/stores/settingStore.ts:129-150 +function getVersionedDefaultValue( + key: K, + param: SettingParams | undefined +): TValue | null { + const defaultsByInstallVersion = param?.defaultsByInstallVersion + if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') { + const installedVersion = get('Comfy.InstalledVersion') + if (installedVersion) { + const sortedVersions = Object.keys(defaultsByInstallVersion).sort( + (a, b) => compareVersions(b, a) + ) + for (const version of sortedVersions) { + if (!isSemVer(version)) continue + if (compareVersions(installedVersion, version) >= 0) { + const versionedDefault = defaultsByInstallVersion[version] + return typeof versionedDefault === 'function' + ? versionedDefault() + : versionedDefault + } + } + } + } + return null +} +``` + +Example versioned defaults from codebase: + +```typescript +// From src/constants/coreSettings.ts:38-40 +{ + id: 'Comfy.Graph.LinkReleaseAction', + defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU, + defaultsByInstallVersion: { + '1.24.1': LinkReleaseTriggerAction.SEARCH_BOX + } +} + +// Another versioned default example +{ + id: 'Comfy.Graph.LinkReleaseAction.Shift', + defaultValue: LinkReleaseTriggerAction.SEARCH_BOX, + defaultsByInstallVersion: { + '1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU + } +} +``` + +### Real Examples from Codebase + +Here are actual settings showing different patterns: + +```typescript +// Number setting with validation +{ + id: 'LiteGraph.Node.TooltipDelay', + name: 'Tooltip Delay', + type: 'number', + attrs: { + min: 100, + max: 3000, + step: 50 + }, + defaultValue: 500, + versionAdded: '1.9.0' +} + +// Hidden system setting for tracking +{ + id: 'Comfy.InstalledVersion', + name: 'The frontend version that was running when the user first installed ComfyUI', + type: 'hidden', + defaultValue: null, + versionAdded: '1.24.0' +} + +// Slider with complex tooltip +{ + id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold', + name: 'Low quality rendering zoom threshold', + tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in.', + type: 'slider', + attrs: { + min: 0.1, + max: 1.0, + step: 0.05 + }, + defaultValue: 0.5 +} +``` + +### New User Version Capture + +The initial installed version is captured for new users to ensure versioned defaults remain stable: + +```typescript +// From src/services/newUserService.ts:49-53 +await settingStore.set( + 'Comfy.InstalledVersion', + __COMFYUI_FRONTEND_VERSION__ +) +``` + +## Practical Patterns for Environment-Based Defaults + +### Dynamic Default Patterns +```typescript +// Device-based default +{ + id: 'Comfy.Example.MobileDefault', + type: 'boolean', + defaultValue: () => /Mobile/i.test(navigator.userAgent) +} + +// Environment-based default +{ + id: 'Comfy.Example.DevMode', + type: 'boolean', + defaultValue: () => import.meta.env.DEV +} + +// Window size based +{ + id: 'Comfy.Example.CompactUI', + type: 'boolean', + defaultValue: () => window.innerWidth < 1024 +} +``` + +### Version-Based Rollout Pattern +```typescript +{ + id: 'Comfy.Example.NewFeature', + type: 'combo', + options: ['legacy', 'enhanced'], + defaultValue: 'legacy', + defaultsByInstallVersion: { + '1.25.0': 'enhanced' + } +} +``` + +## Settings Persistence and Access + +### API Interaction +Values are stored per user via the backend. The store writes through API and falls back to defaults when not set: + +```typescript +// From src/stores/settingStore.ts:73-75 +onChange(settingsById.value[key], newValue, oldValue) +settingValues.value[key] = newValue +await api.storeSetting(key, newValue) +``` + +### Usage in Components +```typescript +const settingStore = useSettingStore() + +// Get setting value (returns computed default if not set by user) +const value = settingStore.get('Comfy.SomeSetting') + +// Update setting value +await settingStore.set('Comfy.SomeSetting', newValue) +``` + + +## Advanced Settings Features + +### Migration and Backward Compatibility + +Settings support migration from deprecated values: + +```typescript +// From src/stores/settingStore.ts:68-69, 172-175 +const newValue = tryMigrateDeprecatedValue( + settingsById.value[key], + clonedValue +) + +// Migration happens during addSetting for existing values: +if (settingValues.value[setting.id] !== undefined) { + settingValues.value[setting.id] = tryMigrateDeprecatedValue( + setting, + settingValues.value[setting.id] + ) +} +``` + +### onChange Callbacks + +Settings can define onChange callbacks that receive the setting definition, new value, and old value: + +```typescript +// From src/stores/settingStore.ts:73, 177 +onChange(settingsById.value[key], newValue, oldValue) // During set() +onChange(setting, get(setting.id), undefined) // During addSetting() +``` + +### Settings UI and Categories + +Settings are automatically grouped for UI based on their `category` or derived from `id`: + +```typescript +{ + id: 'Comfy.Sidebar.Size', + category: ['Appearance', 'Sidebar', 'Size'], + // UI will group this under Appearance > Sidebar > Size +} +``` + +## Related Documentation + +- Feature flag system: `docs/FEATURE_FLAGS.md` +- Settings schema for backend: `src/schemas/apiSchema.ts` (zSettings) +- Server configuration (separate from user settings): `src/constants/serverConfig.ts` + +## Summary + +- **Settings**: User preferences with dynamic/versioned defaults, persisted per user +- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env) +- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases +- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()` \ No newline at end of file diff --git a/docs/SETTINGS_SEQUENCE_DIAGRAM.md b/docs/SETTINGS_SEQUENCE_DIAGRAM.md new file mode 100644 index 000000000..bc5f95129 --- /dev/null +++ b/docs/SETTINGS_SEQUENCE_DIAGRAM.md @@ -0,0 +1,82 @@ +# Settings and Feature Flags Sequence Diagram + +This diagram shows the flow of settings initialization, default resolution, persistence, and feature flags exchange. + +This diagram accurately reflects the actual implementation in the ComfyUI frontend codebase. + +```mermaid +sequenceDiagram + participant User as User + participant Vue as Vue Component + participant Store as SettingStore (Pinia) + participant API as ComfyApi (WebSocket/REST) + participant Backend as Backend + participant NewUserSvc as NewUserService + + Note over Vue,Store: App startup (GraphCanvas.vue) + Vue->>Store: loadSettingValues() + Store->>API: getSettings() + API->>Backend: GET /settings + Backend-->>API: settings map (per-user) + API-->>Store: settings map + Store-->>Vue: loaded + + Vue->>Store: register CORE_SETTINGS (addSetting for each) + loop For each setting registration + Store->>Store: tryMigrateDeprecatedValue(existing value) + Store->>Store: onChange(setting, currentValue, undefined) + end + + Note over Vue,NewUserSvc: New user detection + Vue->>NewUserSvc: initializeIfNewUser(settingStore) + NewUserSvc->>NewUserSvc: checkIsNewUser(settingStore) + alt New user detected + NewUserSvc->>Store: set("Comfy.InstalledVersion", __COMFYUI_FRONTEND_VERSION__) + Store->>Store: tryMigrateDeprecatedValue(newValue) + Store->>Store: onChange(setting, newValue, oldValue) + Store->>API: storeSetting(key, newValue) + API->>Backend: POST /settings/{id} + else Existing user + Note over NewUserSvc: Skip setting installed version + end + + Note over Vue,Store: Component reads a setting + Vue->>Store: get(key) + Store->>Store: exists(key)? + alt User value exists + Store-->>Vue: return stored user value + else Not set by user + Store->>Store: getVersionedDefaultValue(key) + alt Versioned default matched (defaultsByInstallVersion) + Store-->>Vue: return versioned default + else No version match + Store->>Store: evaluate defaultValue (function or constant) + Note over Store: defaultValue can use window size,
locale, env, etc. + Store-->>Vue: return computed default + end + end + + Note over User,Store: User updates a setting + User->>Vue: changes setting in UI + Vue->>Store: set(key, newValue) + Store->>Store: tryMigrateDeprecatedValue(newValue) + Store->>Store: check if newValue === oldValue (early return if same) + Store->>Store: onChange(setting, newValue, oldValue) + Store->>Store: update settingValues[key] + Store->>API: storeSetting(key, newValue) + API->>Backend: POST /settings/{id} + Backend-->>API: 200 OK + API-->>Store: ack + + Note over API,Backend: Feature Flags WebSocket Exchange + API->>Backend: WS connect + API->>Backend: send { type: "feature_flags", data: clientFeatureFlags.json } + Backend-->>API: WS send { type: "feature_flags", data: server flags } + API->>API: store serverFeatureFlags = data + + Note over Vue,API: Feature flag consumption in UI/logic + Vue->>API: serverSupportsFeature(name) + API-->>Vue: boolean (true only if flag === true) + Vue->>API: getServerFeature(name, default) + API-->>Vue: value or default +``` diff --git a/docs/adr/0003-crdt-based-layout-system.md b/docs/adr/0003-crdt-based-layout-system.md new file mode 100644 index 000000000..1eceb2a1f --- /dev/null +++ b/docs/adr/0003-crdt-based-layout-system.md @@ -0,0 +1,156 @@ +# 3. Centralized Layout Management with CRDT + +Date: 2025-08-27 + +## Status + +Proposed + +## Context + +ComfyUI's node graph editor currently suffers from fundamental architectural limitations around spatial data management that prevent us from achieving key product goals. + +### Current Architecture Problems + +The existing system allows each node to directly mutate its position within LiteGraph's canvas renderer. This creates several critical issues: + +1. **Performance Bottlenecks**: UI updates require full graph traversals to detect position changes. Large workflows (100+ nodes) can create bottlenecks during interactions due to this O(n) polling approach. + +2. **Position Conflicts**: Multiple systems (LiteGraph canvas, DOMwidgets.ts overlays) currently compete to control node positions. Future Vue widget overlays will compound this maintenance burden. + +3. **No Collaboration Foundation**: Direct position mutations make concurrent editing impossible—there's no mechanism to merge conflicting position updates from multiple users. + +4. **Renderer Lock-in**: Spatial data is tightly coupled to LiteGraph's canvas implementation, preventing alternative rendering approaches (WebGL, DOM, other libraries, hybrid approaches). + +5. **Inefficient Change Detection**: While LiteGraph provides some events, many operations require polling via changeTracker.ts. The current undo/redo system performs expensive diffs on every interaction rather than using reactive push/pull signals, creating performance bottlenecks and blocking efficient animations and viewport culling. + + This represents a fundamental architectural limitation: diff-based systems scale O(n) with graph complexity (traverse entire structure to detect changes), while signal-based reactive systems scale O(1) with actual changes (data mutations automatically notify subscribers). Modern frameworks (Vue 3, Angular signals, SolidJS) have moved to reactive approaches for precisely this performance reason. + +### Business Context + +- Performance issues emerge with workflow complexity (100+ nodes) +- The AI workflow community increasingly expects collaborative features (similar to Figma, Miro) +- Accessibility requirements will necessitate DOM-based rendering options +- Technical debt compounds with each new spatial feature + +This decision builds on [ADR-0001 (Merge LiteGraph)](0001-merge-litegraph-into-frontend.md), which enables the architectural restructuring proposed here. + +## Decision + +We will implement a centralized layout management system using CRDT (Conflict-free Replicated Data Types) with command pattern architecture to separate spatial data from rendering behavior. + +### Centralized State Management Foundation + +This solution applies proven centralized state management patterns: + +- **Centralized Store**: All spatial data (position, size, bounds, transform) managed in a single CRDT-backed store +- **Command Interface**: All mutations flow through explicit commands rather than direct property access +- **Observer Pattern**: Independent systems (rendering, interaction, layout) subscribe to state changes +- **Domain Separation**: Layout logic completely separated from rendering and UI concerns + +This provides single source of truth, predictable state updates, and natural system decoupling—solving our core architectural problems. + +### Core Architecture + +1. **Centralized Layout Store**: A Yjs CRDT maintains all spatial data in a single authoritative store: + ```typescript + // Instead of: node.position = {x, y} + layoutStore.moveNode(nodeId, {x, y}) + ``` + +2. **Command Pattern**: All spatial mutations flow through explicit commands: + ``` + User Input → Commands → Layout Store → Observer Notifications → Renderers + ``` + +3. **Observer-Based Systems**: Multiple independent systems subscribe to layout changes: + - **Rendering Systems**: LiteGraph canvas, WebGL, DOM accessibility renderers + - **Interaction Systems**: Drag handlers, selection, hover states + - **Layout Systems**: Auto-layout, alignment, distribution + - **Animation Systems**: Smooth transitions, physics simulations + +4. **Reactive Updates**: Store changes propagate through observers, eliminating polling and enabling efficient system coordination. + +### Implementation Strategy + +**Phase 1: Parallel System** +- Build CRDT layout store alongside existing system +- Layout store initially mirrors LiteGraph changes via observers +- Gradually migrate user interactions to use command interface +- Maintain full backward compatibility + +**Phase 2: Inversion of Control** +- CRDT store becomes single source of truth +- LiteGraph receives position updates via reactive subscriptions +- Enable alternative renderers and advanced features + +### Why Centralized State + CRDT? + +This combination provides both architectural and technical benefits: + +**Centralized State Benefits:** +- **Single Source of Truth**: All layout data managed in one place, eliminating conflicts +- **System Decoupling**: Rendering, interaction, and layout systems operate independently +- **Predictable Updates**: Clear data flow makes debugging and testing easier +- **Extensibility**: Easy to add new layout behaviors without modifying existing systems + +**CRDT Benefits:** +- **Conflict Resolution**: Automatic merging eliminates position conflicts between systems +- **Collaboration-Ready**: Built-in support for multi-user editing +- **Eventual Consistency**: Guaranteed convergence to same state across all clients + +**Yjs-Specific Benefits:** +- **Event-Driven**: Native observer pattern removes need for polling +- **Selective Updates**: Only changed nodes trigger system updates +- **Fine-Grained Changes**: Efficient delta synchronization + +## Consequences + +### Positive + +- **Eliminates Polling**: Observer pattern removes O(n) graph traversals, improving performance +- **System Modularity**: Independent systems can be developed, tested, and optimized separately +- **Renderer Flexibility**: Easy to add WebGL, DOM accessibility, or hybrid rendering systems +- **Rich Interactions**: Command pattern enables robust undo/redo, macros, and interaction history +- **Collaboration-Ready**: CRDT foundation enables real-time multi-user editing +- **Conflict Resolution**: Eliminates position "snap-back" behavior between competing systems +- **Better Developer Experience**: Clear separation of concerns and predictable data flow patterns + +### Negative + +- **Learning Curve**: Team must understand CRDT concepts and centralized state management +- **Migration Complexity**: Gradual migration of existing direct property access requires careful coordination +- **Memory Overhead**: Yjs library (~30KB) plus operation history storage +- **CRDT Performance**: CRDTs have computational overhead compared to direct property access +- **Increased Abstraction**: Additional layer between user interactions and visual updates + +### Risk Mitigations + +- Provide comprehensive migration documentation and examples +- Build compatibility layer for gradual, low-risk migration +- Implement operation history pruning for long-running sessions +- Phase implementation to validate approach before full migration + +## Notes + +This centralized state + CRDT architecture follows patterns from modern collaborative applications: + +**Centralized State Management**: Similar to Redux/Vuex patterns in complex web applications, but with CRDT backing for collaboration. This provides predictable state updates while enabling real-time multi-user features. + +**CRDT in Collaboration**: Tools like Figma, Linear, and Notion use similar approaches for real-time collaboration, demonstrating the effectiveness of separating authoritative data from presentation logic. + +**Future Capabilities**: This foundation enables advanced features that would be difficult with the current architecture: +- Macro recording and workflow automation +- Programmatic layout optimization and constraints +- API-driven workflow construction +- Multiple simultaneous renderers (canvas + accessibility DOM) +- Real-time collaborative editing +- Advanced spatial features (physics, animations, auto-layout) + +The architecture provides immediate single-user benefits while creating infrastructure for collaborative and advanced spatial features. + +## References + +- [Yjs Documentation](https://docs.yjs.dev/) +- [CRDTs: The Hard Parts](https://martin.kleppmann.com/2020/07/06/crdt-hard-parts-hydra.html) by Martin Kleppmann +- [Figma's Multiplayer Technology](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/) diff --git a/docs/adr/0004-fork-primevue-ui-library.md b/docs/adr/0004-fork-primevue-ui-library.md new file mode 100644 index 000000000..02bd18736 --- /dev/null +++ b/docs/adr/0004-fork-primevue-ui-library.md @@ -0,0 +1,62 @@ +# 4. Fork PrimeVue UI Library + +Date: 2025-08-27 + +## Status + +Rejected + +## Context + +ComfyUI's frontend requires modifications to PrimeVue components that cannot be achieved through the library's customization APIs. Two specific technical incompatibilities have been identified with the transform-based canvas architecture: + +**Screen Coordinate Hit-Testing Conflicts:** +- PrimeVue components use `getBoundingClientRect()` for screen coordinate calculations that don't account for CSS transforms +- The Slider component directly uses raw `pageX/pageY` coordinates ([lines 102-103](https://github.com/primefaces/primevue/blob/master/packages/primevue/src/slider/Slider.vue#L102-L103)) without transform-aware positioning +- This breaks interaction in transformed coordinate spaces where screen coordinates don't match logical element positions + +**Virtual Canvas Scroll Interference:** +- LiteGraph's infinite canvas uses scroll coordinates semantically for graph navigation via the `DragAndScale` coordinate system +- PrimeVue overlay components automatically trigger `scrollIntoView` behavior which interferes with this virtual positioning +- This issue is documented in [PrimeVue discussion #4270](https://github.com/orgs/primefaces/discussions/4270) where the feature request was made to disable this behavior + +**Historical Overlay Issues:** +- Previous z-index positioning conflicts required manual workarounds (commit `6d4eafb0`) where PrimeVue Dialog components needed `autoZIndex: false` and custom mask styling, later resolved by removing PrimeVue's automatic z-index management entirely + +**Minimal Update Overhead:** +- Analysis of git history shows only 2 PrimeVue version updates in 2+ years, indicating that upstream sync overhead is negligible for this project + +**Future Interaction System Requirements:** +- The ongoing canvas architecture evolution will require more granular control over component interaction and event handling as the transform-based system matures +- Predictable need for additional component modifications beyond current identified issues + +## Decision + +We will **NOT** fork PrimeVue. After evaluation, forking was determined to be unnecessarily complex and costly. + +**Rationale for Rejection:** + +- **Significant Implementation Complexity**: PrimeVue is structured as a monorepo ([primefaces/primevue](https://github.com/primefaces/primevue)) with significant code in a separate monorepo ([PrimeUIX](https://github.com/primefaces/primeuix)). Forking would require importing both repositories whole and selectively pruning or exempting components from our workspace tooling, adding substantial complexity. + +- **Alternative Solutions Available**: The modifications we identified (e.g., scroll interference issues, coordinate system conflicts) have less costly solutions that don't require maintaining a full fork. For example, coordinate issues could be addressed through event interception and synthetic event creation with scaled values. + +- **Maintenance Burden**: Ongoing maintenance and upgrades would be very painful, requiring manual conflict resolution and keeping pace with upstream changes across multiple repositories. + +- **Limited Tooling Support**: There isn't adequate tooling that provides the granularity needed to cleanly manage a PrimeVue fork within our existing infrastructure. + +## Consequences + +### Alternative Approach + +- **Use PrimeVue as External Dependency**: Continue using PrimeVue as a standard npm dependency +- **Targeted Workarounds**: Implement specific solutions for identified issues (coordinate system conflicts, scroll interference) without forking the entire library +- **Selective Component Replacement**: Use libraries like shadcn/ui to replace specific problematic PrimeVue components and adjust them to match our design system +- **Upstream Engagement**: Continue engaging with PrimeVue community for feature requests and bug reports +- **Maintain Flexibility**: Preserve ability to upgrade PrimeVue versions without fork maintenance overhead + +## Notes + +- Technical issues documented in the Context section remain valid concerns +- Solutions will be pursued through targeted fixes rather than wholesale forking +- Future re-evaluation possible if PrimeVue's architecture significantly changes or if alternative tooling becomes available +- This decision prioritizes maintainability and development velocity over maximum customization control diff --git a/docs/adr/README.md b/docs/adr/README.md index 3ebf340d5..5f6e5c2cf 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -11,7 +11,9 @@ An Architecture Decision Record captures an important architectural decision mad | ADR | Title | Status | Date | |-----|-------|--------|------| | [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 | -| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Proposed | 2025-08-25 | +| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 | +| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 | +| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 | ## Creating a New ADR @@ -77,4 +79,4 @@ Optional section for additional information, references, or clarifications. ## Further Reading - [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) by Michael Nygard -- [Architecture Decision Records](https://adr.github.io/) - Collection of ADR resources \ No newline at end of file +- [Architecture Decision Records](https://adr.github.io/) - Collection of ADR resources diff --git a/docs/extensions/development.md b/docs/extensions/development.md index 47c83ecf0..e988d1d45 100644 --- a/docs/extensions/development.md +++ b/docs/extensions/development.md @@ -110,7 +110,7 @@ pnpm build For faster iteration during development, use watch mode: ```bash -npx vite build --watch +pnpm exec vite build --watch ``` Note: Watch mode provides faster rebuilds than full builds, but still no hot reload diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 7cbddbed7..000000000 --- a/eslint.config.js +++ /dev/null @@ -1,101 +0,0 @@ -// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format -import pluginJs from '@eslint/js' -import pluginI18n from '@intlify/eslint-plugin-vue-i18n' -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' -import storybook from 'eslint-plugin-storybook' -import unusedImports from 'eslint-plugin-unused-imports' -import pluginVue from 'eslint-plugin-vue' -import globals from 'globals' -import tseslint from 'typescript-eslint' - -export default [ - { - files: ['src/**/*.{js,mjs,cjs,ts,vue}'] - }, - { - ignores: [ - 'src/scripts/*', - 'src/extensions/core/*', - 'src/types/vue-shim.d.ts', - // Generated files that don't need linting - 'src/types/comfyRegistryTypes.ts', - 'src/types/generatedManagerTypes.ts' - ] - }, - { - languageOptions: { - globals: { - ...globals.browser, - __COMFYUI_FRONTEND_VERSION__: 'readonly' - }, - parser: tseslint.parser, - parserOptions: { - project: ['./tsconfig.json', './tsconfig.eslint.json'], - ecmaVersion: 2020, - sourceType: 'module', - extraFileExtensions: ['.vue'] - } - } - }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - ...pluginVue.configs['flat/recommended'], - eslintPluginPrettierRecommended, - { - files: ['src/**/*.vue'], - languageOptions: { - parserOptions: { - parser: tseslint.parser - } - } - }, - { - plugins: { - 'unused-imports': unusedImports, - '@intlify/vue-i18n': pluginI18n - }, - rules: { - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/prefer-as-const': 'off', - 'unused-imports/no-unused-imports': 'error', - 'vue/no-v-html': 'off', - // i18n rules - '@intlify/vue-i18n/no-raw-text': [ - 'error', - { - // Ignore strings that are: - // 1. Less than 2 characters - // 2. Only symbols/numbers/whitespace (no letters) - // 3. Match specific patterns - ignorePattern: - '^[^a-zA-Z]*$|^.{0,1}$|^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$', - ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'], - // Brand names and technical terms that shouldn't be translated - ignoreText: [ - 'ComfyUI', - 'GitHub', - 'OpenAI', - 'API', - 'URL', - 'JSON', - 'YAML', - 'GPU', - 'CPU', - 'RAM', - 'GB', - 'MB', - 'KB', - 'ms', - 'fps', - 'px', - 'App Data:', - 'App Path:' - ] - } - ] - } - }, - ...storybook.configs['flat/recommended'] -] diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 000000000..dce6a8266 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,228 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import pluginJs from '@eslint/js' +import pluginI18n from '@intlify/eslint-plugin-vue-i18n' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import storybook from 'eslint-plugin-storybook' +import unusedImports from 'eslint-plugin-unused-imports' +import pluginVue from 'eslint-plugin-vue' +import { defineConfig } from 'eslint/config' +import globals from 'globals' +import tseslint from 'typescript-eslint' +import vueParser from 'vue-eslint-parser' + +const extraFileExtensions = ['.vue'] + +export default defineConfig([ + { + ignores: [ + 'src/scripts/*', + 'src/extensions/core/*', + 'src/types/vue-shim.d.ts', + 'src/types/comfyRegistryTypes.ts', + 'src/types/generatedManagerTypes.ts', + '**/vite.config.*.timestamp*', + '**/vitest.config.*.timestamp*', + 'lint-staged.config.js', + 'vitest.litegraph.config.ts' + ] + }, + { + files: ['./**/*.js'], + languageOptions: { + globals: { + ...globals.browser, + __COMFYUI_FRONTEND_VERSION__: 'readonly' + }, + ecmaVersion: 2020, + sourceType: 'module' + }, + rules: { + '@typescript-eslint/no-floating-promises': 'off' + } + }, + { + files: ['./**/*.{ts,mts}'], + languageOptions: { + globals: { + ...globals.browser, + __COMFYUI_FRONTEND_VERSION__: 'readonly' + }, + parserOptions: { + parser: tseslint.parser, + projectService: { + allowDefaultProject: [ + 'vite.config.mts', + 'vite.electron.config.mts', + 'vite.types.config.mts', + 'vitest.litegraph.config.ts' + ] + }, + tsConfigRootDir: import.meta.dirname, + ecmaVersion: 2020, + sourceType: 'module', + extraFileExtensions + } + } + }, + { + files: ['./**/*.vue'], + languageOptions: { + globals: { + ...globals.browser, + __COMFYUI_FRONTEND_VERSION__: 'readonly' + }, + parser: vueParser, + parserOptions: { + parser: tseslint.parser, + projectService: true, + tsConfigRootDir: import.meta.dirname, + ecmaVersion: 2020, + sourceType: 'module', + extraFileExtensions + } + } + }, + pluginJs.configs.recommended, + tseslint.configs.recommended, + pluginVue.configs['flat/recommended'], + eslintPluginPrettierRecommended, + storybook.configs['flat/recommended'], + { + plugins: { + 'unused-imports': unusedImports, + // @ts-expect-error Bad types in the plugin + '@intlify/vue-i18n': pluginI18n + }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/prefer-as-const': 'off', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-empty-object-type': [ + 'error', + { + allowInterfaces: 'always' + } + ], + 'unused-imports/no-unused-imports': 'error', + 'vue/no-v-html': 'off', + // Enforce dark-theme: instead of dark: prefix + 'vue/no-restricted-class': ['error', '/^dark:/'], + 'vue/multi-word-component-names': 'off', // TODO: fix + 'vue/no-template-shadow': 'off', // TODO: fix + /* Toggle on to do additional until we can clean up existing violations. + 'vue/no-unused-emit-declarations': 'error', + 'vue/no-unused-properties': 'error', + 'vue/no-unused-refs': 'error', + 'vue/no-use-v-else-with-v-for': 'error', + 'vue/no-useless-v-bind': 'error', + // */ + 'vue/one-component-per-file': 'off', // TODO: fix + 'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile + // Restrict deprecated PrimeVue components + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'primevue/calendar', + message: + 'Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from "primevue/datepicker"' + }, + { + name: 'primevue/dropdown', + message: + 'Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from "primevue/select"' + }, + { + name: 'primevue/inputswitch', + message: + 'InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from "primevue/toggleswitch"' + }, + { + name: 'primevue/overlaypanel', + message: + 'OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from "primevue/popover"' + }, + { + name: 'primevue/sidebar', + message: + 'Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from "primevue/drawer"' + } + ] + } + ], + // i18n rules + '@intlify/vue-i18n/no-raw-text': [ + 'error', + { + // Ignore strings that are: + // 1. Less than 2 characters + // 2. Only symbols/numbers/whitespace (no letters) + // 3. Match specific patterns + ignorePattern: + '^[^a-zA-Z]*$|^.{0,1}$|^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$', + ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'], + // Brand names and technical terms that shouldn't be translated + ignoreText: [ + 'ComfyUI', + 'GitHub', + 'OpenAI', + 'API', + 'URL', + 'JSON', + 'YAML', + 'GPU', + 'CPU', + 'RAM', + 'GB', + 'MB', + 'KB', + 'ms', + 'fps', + 'px', + 'App Data:', + 'App Path:' + ] + } + ] + } + }, + { + files: ['tests-ui/**/*'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false } + ] + } + }, + { + files: ['**/*.spec.ts'], + ignores: ['browser_tests/tests/**/*.spec.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: '.spec.ts files are only allowed under browser_tests/tests/' + } + ] + } + }, + { + files: ['browser_tests/tests/**/*.test.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: + '.test.ts files are not allowed in browser_tests/tests/; use .spec.ts instead' + } + ] + } + } +]) diff --git a/index.html b/index.html index 5aa3492c8..17e389555 100644 --- a/index.html +++ b/index.html @@ -9,9 +9,9 @@ - - - + + + diff --git a/knip.config.ts b/knip.config.ts index 81333a2c3..3022a0af5 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,82 +1,67 @@ import type { KnipConfig } from 'knip' const config: KnipConfig = { - entry: [ - 'src/main.ts', - 'vite.config.mts', - 'vite.electron.config.mts', - 'vite.types.config.mts', - 'eslint.config.js', - 'tailwind.config.js', - 'postcss.config.js', - 'playwright.config.ts', - 'playwright.i18n.config.ts', - 'vitest.config.ts', - 'scripts/**/*.{js,ts}' - ], - project: [ - 'src/**/*.{js,ts,vue}', - 'tests-ui/**/*.{js,ts,vue}', - 'browser_tests/**/*.{js,ts}', - 'scripts/**/*.{js,ts}' - ], - ignore: [ - // Generated files - 'dist/**', - 'types/**', - 'node_modules/**', - // Config files that might not show direct usage - '.husky/**', - // Temporary or cache files - '.vite/**', - 'coverage/**', - // i18n config - '.i18nrc.cjs', - // Test setup files - 'browser_tests/globalSetup.ts', - 'browser_tests/globalTeardown.ts', - 'browser_tests/utils/**', - // Scripts - 'scripts/**', - // Vite config files - 'vite.electron.config.mts', - 'vite.types.config.mts', - // Auto generated manager types - 'src/types/generatedManagerTypes.ts', - // Design system components (may not be used immediately) - 'src/components/button/IconGroup.vue', - 'src/components/button/MoreButton.vue', - 'src/components/button/TextButton.vue', - 'src/components/card/CardTitle.vue', - 'src/components/card/CardDescription.vue', - 'src/components/input/SingleSelect.vue' - ], - ignoreExportsUsedInFile: true, - // Vue-specific configuration - vue: true, - // Only check for unused files, disable all other rules - // TODO: Gradually enable other rules - see https://github.com/Comfy-Org/ComfyUI_frontend/issues/4888 - rules: { - binaries: 'off', - classMembers: 'off', - dependencies: 'off', - devDependencies: 'off', - duplicates: 'off', - enumMembers: 'off', - exports: 'off', - nsExports: 'off', - nsTypes: 'off', - types: 'off', - unlisted: 'off' - }, - // Include dependencies analysis - includeEntryExports: true, - // Workspace configuration for monorepo-like structure workspaces: { '.': { - entry: ['src/main.ts'] + entry: [ + '{build,scripts}/**/*.{js,ts}', + 'src/assets/css/style.css', + 'src/main.ts', + 'src/scripts/ui/menu/index.ts', + 'src/types/index.ts' + ], + project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'] + }, + 'packages/tailwind-utils': { + project: ['src/**/*.{js,ts}'] + }, + 'packages/design-system': { + entry: ['src/**/*.ts'], + project: ['src/**/*.{js,ts}', '*.{js,ts,mts}'] } - } + }, + ignoreDependencies: [ + // Weird importmap things + '@iconify/json', + '@primeuix/forms', + '@primeuix/styled', + '@primeuix/utils', + '@primevue/icons', + // Dev + '@trivago/prettier-plugin-sort-imports' + ], + ignore: [ + // Auto generated manager types + 'src/workbench/extensions/manager/types/generatedManagerTypes.ts', + 'src/types/comfyRegistryTypes.ts', + // Used by a custom node (that should move off of this) + 'src/scripts/ui/components/splitButton.ts' + ], + compilers: { + // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 + css: (text: string) => + [ + ...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g) + ].join('\n') + }, + vite: { + config: ['vite?(.*).config.mts'] + }, + vitest: { + config: ['vitest?(.*).config.ts'], + entry: [ + '**/*.{bench,test,test-d,spec}.?(c|m)[jt]s?(x)', + '**/__mocks__/**/*.[jt]s?(x)' + ] + }, + playwright: { + config: ['playwright?(.*).config.ts'], + entry: ['**/*.@(spec|test).?(c|m)[jt]s?(x)', 'browser_tests/**/*.ts'] + }, + tags: [ + '-knipIgnoreUnusedButUsedByCustomNodes', + '-knipIgnoreUnusedButUsedByVueNodesBranch' + ] } export default config diff --git a/lint-staged.config.js b/lint-staged.config.js index 2d1a6f051..a1a08a247 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,15 +1,8 @@ export default { - './**/*.js': (stagedFiles) => formatAndEslint(stagedFiles), + './**/*.js': 'pnpm exec eslint --cache --fix', - './**/*.{ts,tsx,vue,mts}': (stagedFiles) => [ - ...formatAndEslint(stagedFiles), - 'vue-tsc --noEmit' - ] -} - -function formatAndEslint(fileNames) { - return [ - `eslint --fix ${fileNames.join(' ')}`, - `prettier --write ${fileNames.join(' ')}` + './**/*.{ts,tsx,vue,mts}': [ + 'pnpm exec eslint --cache --fix', + 'pnpm exec prettier --cache --write' ] } diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs new file mode 100644 index 000000000..0f3808700 --- /dev/null +++ b/lint-staged.config.mjs @@ -0,0 +1,15 @@ +export default { + './**/*.js': (stagedFiles) => formatAndEslint(stagedFiles), + + './**/*.{ts,tsx,vue,mts}': (stagedFiles) => [ + ...formatAndEslint(stagedFiles), + 'pnpm typecheck' + ] +} + +function formatAndEslint(fileNames) { + return [ + `pnpm exec eslint --cache --fix ${fileNames.join(' ')}`, + `pnpm exec prettier --cache --write ${fileNames.join(' ')}` + ] +} diff --git a/nx.json b/nx.json new file mode 100644 index 000000000..675308576 --- /dev/null +++ b/nx.json @@ -0,0 +1,40 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "plugins": [ + { + "plugin": "@nx/eslint/plugin", + "options": { + "targetName": "lint" + } + }, + { + "plugin": "@nx/storybook/plugin", + "options": { + "serveStorybookTargetName": "storybook", + "buildStorybookTargetName": "build-storybook", + "testStorybookTargetName": "test-storybook", + "staticStorybookTargetName": "static-storybook" + } + }, + { + "plugin": "@nx/vite/plugin", + "options": { + "buildTargetName": "build", + "testTargetName": "test", + "serveTargetName": "serve", + "devTargetName": "dev", + "previewTargetName": "preview", + "serveStaticTargetName": "serve-static", + "typecheckTargetName": "typecheck", + "buildDepsTargetName": "build-deps", + "watchDepsTargetName": "watch-deps" + } + }, + { + "plugin": "@nx/playwright/plugin", + "options": { + "targetName": "e2e" + } + } + ] +} diff --git a/package.json b/package.json index 61f4633dd..da9b83ebe 100644 --- a/package.json +++ b/package.json @@ -1,104 +1,113 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.26.7", + "version": "1.28.3", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", "description": "Official front-end implementation of ComfyUI", "license": "GPL-3.0-only", "scripts": { - "dev": "vite", - "dev:electron": "vite --config vite.electron.config.mts", - "build": "pnpm typecheck && vite build", - "build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js", + "dev": "nx serve", + "dev:electron": "nx serve --config vite.electron.config.mts", + "build": "pnpm typecheck && nx build", + "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", "zipdist": "node scripts/zipdist.js", "typecheck": "vue-tsc --noEmit", - "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache", + "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different", "format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache", - "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'", + "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different", "format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'", - "test:browser": "npx playwright test", - "test:unit": "vitest run tests-ui/tests", - "test:component": "vitest run src/components/", - "preinstall": "npx only-allow pnpm", + "test:all": "nx run test", + "test:browser": "pnpm exec nx e2e", + "test:component": "nx run test src/components/", + "test:litegraph": "vitest run --config vitest.litegraph.config.ts", + "test:unit": "nx run test tests-ui/tests", + "preinstall": "pnpm dlx only-allow pnpm", "prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true", - "preview": "vite preview", + "preview": "nx preview", "lint": "eslint src --cache", "lint:fix": "eslint src --cache --fix", + "lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache", + "lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix", "lint:no-cache": "eslint src", "lint:fix:no-cache": "eslint src --fix", "knip": "knip --cache", "knip:no-cache": "knip", "locale": "lobe-i18n locale", - "collect-i18n": "playwright test --config=playwright.i18n.config.ts", + "collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts", "json-schema": "tsx scripts/generate-json-schema.ts", - "storybook": "storybook dev -p 6006", + "storybook": "nx storybook -p 6006", "build-storybook": "storybook build" }, "devDependencies": { - "@eslint/js": "^9.8.0", - "@executeautomation/playwright-mcp-server": "^1.0.5", - "@iconify/json": "^2.2.245", - "@iconify/tailwind": "^1.2.0", - "@intlify/eslint-plugin-vue-i18n": "^3.2.0", - "@lobehub/i18n-cli": "^1.20.0", + "@eslint/js": "^9.35.0", + "@intlify/eslint-plugin-vue-i18n": "^4.1.0", + "@lobehub/i18n-cli": "^1.25.1", + "@nx/eslint": "21.4.1", + "@nx/playwright": "21.4.1", + "@nx/storybook": "21.4.1", + "@nx/vite": "21.4.1", "@pinia/testing": "^0.1.5", "@playwright/test": "^1.52.0", "@storybook/addon-docs": "^9.1.1", "@storybook/vue3": "^9.1.1", "@storybook/vue3-vite": "^9.1.1", + "@tailwindcss/vite": "^4.1.12", "@trivago/prettier-plugin-sort-imports": "^5.2.0", - "@types/dompurify": "^3.0.5", "@types/fs-extra": "^11.0.4", + "@types/jsdom": "^21.1.7", "@types/node": "^20.14.8", "@types/semver": "^7.7.0", "@types/three": "^0.169.0", "@vitejs/plugin-vue": "^5.1.4", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.0.0", "@vue/test-utils": "^2.4.6", - "autoprefixer": "^10.4.19", - "chalk": "^5.3.0", - "commander": "^14.0.0", - "eslint": "^9.12.0", - "eslint-config-prettier": "^10.1.2", - "eslint-plugin-prettier": "^5.2.6", - "eslint-plugin-storybook": "^9.1.1", - "eslint-plugin-unused-imports": "^4.1.4", - "eslint-plugin-vue": "^9.27.0", + "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-storybook": "^9.1.6", + "eslint-plugin-unused-imports": "^4.2.0", + "eslint-plugin-vue": "^10.4.0", "fs-extra": "^11.2.0", "globals": "^15.9.0", "happy-dom": "^15.11.0", "husky": "^9.0.11", - "identity-obj-proxy": "^3.0.0", - "ink": "^4.2.0", + "jiti": "2.4.2", + "jsdom": "^26.1.0", "knip": "^5.62.0", "lint-staged": "^15.2.7", - "lucide-vue-next": "^0.540.0", - "postcss": "^8.4.39", + "nx": "21.4.1", "prettier": "^3.3.2", - "react": "^18.3.1", - "react-reconciler": "^0.29.2", - "storybook": "^9.1.1", - "tailwindcss": "^3.4.4", + "storybook": "^9.1.6", + "tailwindcss": "^4.1.12", + "tailwindcss-primeui": "^0.6.1", "tsx": "^4.15.6", + "tw-animate-css": "^1.3.8", "typescript": "^5.4.5", - "typescript-eslint": "^8.0.0", + "typescript-eslint": "^8.44.0", "unplugin-icons": "^0.22.0", "unplugin-vue-components": "^0.28.0", "uuid": "^11.1.0", "vite": "^5.4.19", - "vite-plugin-dts": "^4.3.0", + "vite-plugin-dts": "^4.5.4", "vite-plugin-html": "^3.2.2", "vite-plugin-vue-devtools": "^7.7.6", - "vitest": "^2.0.0", - "vue-tsc": "^2.1.10", + "vitest": "^3.2.4", + "vue-component-type-helpers": "^3.0.7", + "vue-eslint-parser": "^10.2.0", + "vue-tsc": "^3.0.7", "zip-dir": "^2.0.0", "zod-to-json-schema": "^3.24.1" }, "dependencies": { "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", - "@comfyorg/comfyui-electron-types": "^0.4.43", + "@comfyorg/comfyui-electron-types": "0.4.73-0", + "@comfyorg/design-system": "workspace:*", + "@comfyorg/tailwind-utils": "workspace:*", + "@iconify/json": "^2.2.380", "@primeuix/forms": "0.0.2", "@primeuix/styled": "0.3.2", "@primeuix/utils": "^0.3.2", @@ -106,7 +115,6 @@ "@primevue/forms": "^4.2.5", "@primevue/icons": "4.2.5", "@primevue/themes": "^4.2.5", - "@sentry/core": "^10.5.0", "@sentry/vue": "^8.48.0", "@tiptap/core": "^2.10.4", "@tiptap/extension-link": "^2.10.4", @@ -116,11 +124,13 @@ "@tiptap/extension-table-row": "^2.10.4", "@tiptap/starter-kit": "^2.10.4", "@vueuse/core": "^11.0.0", + "@vueuse/integrations": "^13.9.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-serialize": "^0.13.0", "@xterm/xterm": "^5.5.0", "algoliasearch": "^5.21.0", "axios": "^1.8.2", + "chart.js": "^4.5.0", "dompurify": "^3.2.5", "dotenv": "^16.4.5", "es-toolkit": "^1.39.9", @@ -136,6 +146,7 @@ "pinia": "^2.1.7", "primeicons": "^7.0.0", "primevue": "^4.2.5", + "reka-ui": "^2.5.0", "semver": "^7.7.2", "three": "^0.170.0", "tiptap-markdown": "^0.8.10", @@ -143,6 +154,7 @@ "vue-i18n": "^9.14.3", "vue-router": "^4.4.3", "vuefire": "^3.2.1", + "yjs": "^13.6.27", "zod": "^3.23.8", "zod-validation-error": "^3.3.0" } diff --git a/packages/design-system/package.json b/packages/design-system/package.json new file mode 100644 index 000000000..e2868d054 --- /dev/null +++ b/packages/design-system/package.json @@ -0,0 +1,31 @@ +{ + "name": "@comfyorg/design-system", + "version": "1.0.0", + "description": "Shared design system for ComfyUI Frontend", + "type": "module", + "exports": { + "./tailwind-config": { + "import": "./tailwind.config.ts", + "types": "./tailwind.config.ts" + }, + "./css/*": "./src/css/*" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "nx": { + "tags": [ + "scope:shared", + "type:design" + ] + }, + "dependencies": { + "@iconify-json/lucide": "^1.1.178", + "@iconify/tailwind": "^1.1.3" + }, + "devDependencies": { + "tailwindcss": "^3.4.17", + "typescript": "^5.4.5" + }, + "packageManager": "pnpm@10.17.1" +} diff --git a/packages/design-system/src/css/fonts.css b/packages/design-system/src/css/fonts.css new file mode 100644 index 000000000..cea388ee7 --- /dev/null +++ b/packages/design-system/src/css/fonts.css @@ -0,0 +1,17 @@ +/* Inter Font Family */ + +@font-face { + font-family: 'Inter'; + src: url('/fonts/inter-latin-normal.woff2') format('woff2'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Inter'; + src: url('/fonts/inter-latin-italic.woff2') format('woff2'); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css new file mode 100644 index 000000000..0f2bca812 --- /dev/null +++ b/packages/design-system/src/css/style.css @@ -0,0 +1,1003 @@ +@layer theme, base, primevue, components, utilities; + +@import './fonts.css'; +@import 'tailwindcss/theme' layer(theme); +@import 'tailwindcss/utilities' layer(utilities); +@import 'tw-animate-css'; + +@plugin 'tailwindcss-primeui'; + +@config '../../tailwind.config.ts'; + +:root { + --fg-color: #000; + --bg-color: #fff; + --comfy-menu-bg: #353535; + --comfy-menu-secondary-bg: #292929; + --comfy-topbar-height: 2.5rem; + --comfy-input-bg: #222; + --input-text: #ddd; + --descrip-text: #999; + --drag-text: #ccc; + --error-text: #ff4444; + --border-color: #4e4e4e; + --tr-even-bg-color: #222; + --tr-odd-bg-color: #353535; + --primary-bg: #236692; + --primary-fg: #ffffff; + --primary-hover-bg: #3485bb; + --primary-hover-fg: #ffffff; + --content-bg: #e0e0e0; + --content-fg: #000; + --content-hover-bg: #adadad; + --content-hover-fg: #000; + + /* Code styling colors for help menu*/ + --code-text-color: rgba(0, 122, 255, 1); + --code-bg-color: rgba(96, 165, 250, 0.2); + --code-block-bg-color: rgba(60, 60, 60, 0.12); +} + +@media (prefers-color-scheme: dark) { + :root { + --fg-color: #fff; + --bg-color: #202020; + --content-bg: #4e4e4e; + --content-fg: #fff; + --content-hover-bg: #222; + --content-hover-fg: #fff; + } +} + +@theme { + --text-xxs: 0.625rem; + --text-xxs--line-height: calc(1 / 0.625); + + /* Font Families */ + --font-inter: 'Inter', sans-serif; + + /* Palette Colors */ + --color-charcoal-100: #55565e; + --color-charcoal-200: #494a50; + --color-charcoal-300: #3c3d42; + --color-charcoal-400: #313235; + --color-charcoal-500: #2d2e32; + --color-charcoal-600: #262729; + --color-charcoal-700: #202121; + --color-charcoal-800: #171718; + + --color-neutral-550: #636363; + + --color-stone-100: #444444; + --color-stone-200: #828282; + --color-stone-300: #bbbbbb; + + --color-ivory-100: #fdfbfa; + --color-ivory-200: #faf9f5; + --color-ivory-300: #f0eee6; + + --color-gray-100: #f3f3f3; + --color-gray-200: #e9e9e9; + --color-gray-300: #e1e1e1; + --color-gray-400: #d9d9d9; + --color-gray-500: #c5c5c5; + --color-gray-600: #b4b4b4; + --color-gray-700: #a0a0a0; + --color-gray-800: #8a8a8a; + + --color-sand-100: #e1ded5; + --color-sand-200: #d6cfc2; + --color-sand-300: #888682; + + --color-pure-white: #ffffff; + + --color-slate-100: #9c9eab; + --color-slate-200: #9fa2bd; + --color-slate-300: #5b5e7d; + + --color-brand-yellow: #f0ff41; + --color-brand-blue: #172dd7; + + --color-blue-100: #0b8ce9; + --color-blue-200: #31b9f4; + --color-success-100: #00cd72; + --color-success-200: #47e469; + --color-warning-100: #fd9903; + --color-warning-200: #fcbf64; + --color-danger-100: #c02323; + --color-danger-200: #d62952; + + --color-coral-red-600: #973a40; + --color-coral-red-500: #c53f49; + --color-coral-red-400: #dd424e; + + --color-bypass: #6a246a; + --color-error: #962a2a; + + --color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3); + --color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15); + --color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1); + --color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4); + + /* PrimeVue pulled colors */ + --color-muted: var(--p-text-muted-color); + --color-highlight: var(--p-primary-color); + + /* Special Colors (temporary) */ + --color-dark-elevation-1.5: rgba(from white r g b/ 0.015); + --color-dark-elevation-2: rgba(from white r g b / 0.03); +} + +@theme inline { + --color-node-component-surface: var(--color-charcoal-600); + --color-node-component-surface-highlight: var(--color-slate-100); + --color-node-component-surface-hovered: var(--color-charcoal-400); + --color-node-component-surface-selected: var(--color-charcoal-200); + --color-node-stroke: var(--color-stone-100); +} + +@custom-variant dark-theme { + .dark-theme & { + @slot; + } +} + +@utility scrollbar-hide { + scrollbar-width: none; + &::-webkit-scrollbar { + width: 1px; + } + &::-webkit-scrollbar-thumb { + background-color: transparent; + } +} + +/* Everthing below here to be cleaned up over time. */ + +body { + width: 100vw; + height: 100vh; + margin: 0; + overflow: hidden; + background: var(--bg-color) var(--bg-img); + color: var(--fg-color); + min-height: -webkit-fill-available; + max-height: -webkit-fill-available; + min-width: -webkit-fill-available; + max-width: -webkit-fill-available; + font-family: Arial, sans-serif; +} + +.comfy-multiline-input { + background-color: var(--comfy-input-bg); + color: var(--input-text); + overflow: hidden; + overflow-y: auto; + padding: 2px; + resize: none; + border: none; + box-sizing: border-box; + font-size: var(--comfy-textarea-font-size); +} + +.comfy-markdown { + /* We assign the textarea and the Tiptap editor to the same CSS grid area to stack them on top of one another. */ + display: grid; +} + +.comfy-markdown > textarea { + grid-area: 1 / 1 / 2 / 2; +} + +.comfy-markdown .tiptap { + grid-area: 1 / 1 / 2 / 2; + background-color: var(--comfy-input-bg); + color: var(--input-text); + overflow: hidden; + overflow-y: auto; + resize: none; + border: none; + box-sizing: border-box; + font-size: var(--comfy-textarea-font-size); + height: 100%; + padding: 0.5em; +} + +.comfy-markdown.editing .tiptap { + display: none; +} + +.comfy-markdown .tiptap :first-child { + margin-top: 0; +} + +.comfy-markdown .tiptap :last-child { + margin-bottom: 0; +} + +.comfy-markdown .tiptap blockquote { + border-left: medium solid; + margin-left: 1em; + padding-left: 0.5em; +} + +.comfy-markdown .tiptap pre { + border: thin dotted; + border-radius: 0.5em; + margin: 0.5em; + padding: 0.5em; +} + +.comfy-markdown .tiptap table { + border-collapse: collapse; +} + +.comfy-markdown .tiptap th { + text-align: left; + background: var(--comfy-menu-bg); +} + +.comfy-markdown .tiptap th, +.comfy-markdown .tiptap td { + padding: 0.5em; + border: thin solid; +} + +/* Shared markdown content styling for consistent rendering across components */ +.comfy-markdown-content { + /* Typography */ + font-size: 0.875rem; /* text-sm */ + line-height: 1.6; + word-wrap: break-word; +} + +/* Headings */ +.comfy-markdown-content h1 { + font-size: 22px; /* text-[22px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h1:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h2 { + font-size: 18px; /* text-[18px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h2:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h3 { + font-size: 16px; /* text-[16px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h3:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h4, +.comfy-markdown-content h5, +.comfy-markdown-content h6 { + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h4:first-child, +.comfy-markdown-content h5:first-child, +.comfy-markdown-content h6:first-child { + margin-top: 0; /* first:mt-0 */ +} + +/* Paragraphs */ +.comfy-markdown-content p { + margin: 0 0 0.5em; +} + +.comfy-markdown-content p:last-child { + margin-bottom: 0; +} + +/* First child reset */ +.comfy-markdown-content *:first-child { + margin-top: 0; /* mt-0 */ +} + +/* Lists */ +.comfy-markdown-content ul, +.comfy-markdown-content ol { + padding-left: 2rem; /* pl-8 */ + margin: 0.5rem 0; /* my-2 */ +} + +/* Nested lists */ +.comfy-markdown-content ul ul, +.comfy-markdown-content ol ol, +.comfy-markdown-content ul ol, +.comfy-markdown-content ol ul { + padding-left: 1.5rem; /* pl-6 */ + margin: 0.5rem 0; /* my-2 */ +} + +.comfy-markdown-content li { + margin: 0.5rem 0; /* my-2 */ +} + +/* Code */ +.comfy-markdown-content code { + color: var(--code-text-color); + background-color: var(--code-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */ + font-family: monospace; +} + +.comfy-markdown-content pre { + background-color: var(--code-block-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 1rem; /* p-4 */ + margin: 1rem 0; /* my-4 */ + overflow-x: auto; /* overflow-x-auto */ +} + +.comfy-markdown-content pre code { + background-color: transparent; /* bg-transparent */ + padding: 0; /* p-0 */ + color: var(--p-text-color); +} + +/* Tables */ +.comfy-markdown-content table { + width: 100%; /* w-full */ + border-collapse: collapse; /* border-collapse */ +} + +.comfy-markdown-content th, +.comfy-markdown-content td { + padding: 0.5rem; /* px-2 py-2 */ +} + +.comfy-markdown-content th { + color: var(--fg-color); +} + +.comfy-markdown-content td { + color: var(--drag-text); +} + +.comfy-markdown-content tr { + border-bottom: 1px solid var(--content-bg); +} + +.comfy-markdown-content tr:last-child { + border-bottom: none; +} + +.comfy-markdown-content thead { + border-bottom: 1px solid var(--p-text-color); +} + +/* Links */ +.comfy-markdown-content a { + color: var(--drag-text); + text-decoration: underline; +} + +/* Media */ +.comfy-markdown-content img, +.comfy-markdown-content video { + max-width: 100%; /* max-w-full */ + height: auto; /* h-auto */ + display: block; /* block */ + margin-bottom: 1rem; /* mb-4 */ +} + +/* Blockquotes */ +.comfy-markdown-content blockquote { + border-left: 3px solid var(--p-primary-color, var(--primary-bg)); + padding-left: 0.75em; + margin: 0.5em 0; + opacity: 0.8; +} + +/* Horizontal rule */ +.comfy-markdown-content hr { + border: none; + border-top: 1px solid var(--p-border-color, var(--border-color)); + margin: 1em 0; +} + +/* Strong and emphasis */ +.comfy-markdown-content strong { + font-weight: bold; +} + +.comfy-markdown-content em { + font-style: italic; +} + +.comfy-modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 100; /* Sit on top */ + padding: 30px 30px 10px 30px; + background-color: var(--comfy-menu-bg); /* Modal background */ + color: var(--error-text); + box-shadow: 0 0 20px #888888; + border-radius: 10px; + top: 50%; + left: 50%; + max-width: 80vw; + max-height: 80vh; + transform: translate(-50%, -50%); + overflow: hidden; + justify-content: center; + font-family: monospace; + font-size: 15px; +} + +.comfy-modal-content { + display: flex; + flex-direction: column; +} + +.comfy-modal p { + overflow: auto; + white-space: pre-line; /* This will respect line breaks */ + margin-bottom: 20px; /* Add some margin between the text and the close button*/ +} + +.comfy-modal select, +.comfy-modal input[type='button'], +.comfy-modal input[type='checkbox'] { + margin: 3px 3px 3px 4px; +} + +.comfy-menu { + font-size: 15px; + position: absolute; + top: 50%; + right: 0; + text-align: center; + z-index: 999; + width: 190px; + display: flex; + flex-direction: column; + align-items: center; + color: var(--descrip-text); + background-color: var(--comfy-menu-bg); + font-family: sans-serif; + padding: 10px; + border-radius: 0 8px 8px 8px; + box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4); +} + +.comfy-menu-header { + display: flex; +} + +.comfy-menu-actions { + display: flex; + gap: 3px; + align-items: center; + height: 20px; + position: relative; + top: -1px; + font-size: 22px; +} + +.comfy-menu .comfy-menu-actions button { + background-color: rgba(0, 0, 0, 0); + padding: 0; + border: none; + cursor: pointer; + font-size: inherit; +} + +.comfy-menu .comfy-menu-actions .comfy-settings-btn { + font-size: 0.6em; +} + +button.comfy-close-menu-btn { + font-size: 1em; + line-height: 12px; + color: #ccc; + position: relative; + top: -1px; +} + +.comfy-menu-queue-size { + flex: auto; +} + +.comfy-menu button, +.comfy-modal button { + font-size: 20px; +} + +.comfy-menu-btns { + margin-bottom: 10px; + width: 100%; +} + +.comfy-menu-btns button { + font-size: 10px; + width: 50%; + color: var(--descrip-text) !important; +} + +.comfy-menu > button { + width: 100%; +} + +.comfy-btn, +.comfy-menu > button, +.comfy-menu-btns button, +.comfy-menu .comfy-list button, +.comfy-modal button { + color: var(--input-text); + background-color: var(--comfy-input-bg); + border-width: initial; + border-radius: 8px; + border-color: var(--border-color); + border-style: solid; + margin-top: 2px; +} + +.comfy-btn:hover:not(:disabled), +.comfy-menu > button:hover, +.comfy-menu-btns button:hover, +.comfy-menu .comfy-list button:hover, +.comfy-modal button:hover, +.comfy-menu-actions button:hover { + filter: brightness(1.2); + will-change: transform; + cursor: pointer; +} + +span.drag-handle { + width: 10px; + height: 20px; + display: inline-block; + overflow: hidden; + line-height: 5px; + padding: 3px 4px; + cursor: move; + vertical-align: middle; + margin-top: -0.4em; + margin-left: -0.2em; + font-size: 12px; + font-family: sans-serif; + letter-spacing: 2px; + color: var(--drag-text); + text-shadow: 1px 0 1px black; + touch-action: none; +} + +span.drag-handle::after { + content: '.. .. ..'; +} + +.comfy-queue-btn { + width: 100%; +} + +.comfy-list { + color: var(--descrip-text); + background-color: var(--comfy-menu-bg); + margin-bottom: 10px; + border-color: var(--border-color); + border-style: solid; +} + +.comfy-list-items { + overflow-y: scroll; + max-height: 100px; + min-height: 25px; + background-color: var(--comfy-input-bg); + padding: 5px; +} + +.comfy-list h4 { + min-width: 160px; + margin: 0; + padding: 3px; + font-weight: normal; +} + +.comfy-list-items button { + font-size: 10px; +} + +.comfy-list-actions { + margin: 5px; + display: flex; + gap: 5px; + justify-content: center; +} + +.comfy-list-actions button { + font-size: 12px; +} + +button.comfy-queue-btn { + margin: 6px 0 !important; +} + +.comfy-modal.comfy-settings, +.comfy-modal.comfy-manage-templates { + text-align: center; + font-family: sans-serif; + color: var(--descrip-text); + z-index: 99; +} + +.comfy-modal.comfy-settings input[type='range'] { + vertical-align: middle; +} + +.comfy-modal.comfy-settings input[type='range'] + input[type='number'] { + width: 3.5em; +} + +.comfy-modal input, +.comfy-modal select { + color: var(--input-text); + background-color: var(--comfy-input-bg); + border-radius: 8px; + border-color: var(--border-color); + border-style: solid; + font-size: inherit; +} + +.comfy-tooltip-indicator { + text-decoration: underline; + text-decoration-style: dashed; +} + +@media only screen and (max-height: 850px) { + .comfy-menu { + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + border-radius: 0; + } + + .comfy-menu span.drag-handle { + display: none; + } + + .comfy-menu-queue-size { + flex: unset; + } + + .comfy-menu-header { + justify-content: space-between; + } + .comfy-menu-actions { + gap: 10px; + font-size: 28px; + } +} + +/* Input popup */ + +.graphdialog { + min-height: 1em; + background-color: var(--comfy-menu-bg); + z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */ +} + +.graphdialog .name { + font-size: 14px; + font-family: sans-serif; + color: var(--descrip-text); +} + +.graphdialog button { + margin-top: unset; + vertical-align: unset; + height: 1.6em; + padding-right: 8px; +} + +.graphdialog input, +.graphdialog textarea, +.graphdialog select { + background-color: var(--comfy-input-bg); + border: 2px solid; + border-color: var(--border-color); + color: var(--input-text); + border-radius: 12px 0 0 12px; +} + +/* Dialogs */ + +dialog { + box-shadow: 0 0 20px #888888; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.5); +} + +.comfy-dialog.comfyui-dialog.comfy-modal { + top: 0; + left: 0; + right: 0; + bottom: 0; + transform: none; +} + +.comfy-dialog.comfy-modal { + font-family: Arial, sans-serif; + border-color: var(--bg-color); + box-shadow: none; + border: 2px solid var(--border-color); +} + +.comfy-dialog .comfy-modal-content { + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + color: var(--fg-color); +} + +.comfy-dialog .comfy-modal-content h3 { + margin-top: 0; +} + +.comfy-dialog .comfy-modal-content > p { + width: 100%; +} + +.comfy-dialog .comfy-modal-content > .comfyui-button { + flex: 1; + justify-content: center; +} + +/* Context menu */ + +.litegraph .dialog { + z-index: 1; + font-family: Arial, sans-serif; +} + +.litegraph .litemenu-entry.has_submenu { + position: relative; + padding-right: 20px; +} + +.litemenu-entry.has_submenu::after { + content: '>'; + position: absolute; + top: 0; + right: 2px; +} + +.litegraph.litecontextmenu, +.litegraph.litecontextmenu.dark { + z-index: 9999 !important; + background-color: var(--comfy-menu-bg) !important; +} + +.litegraph.litecontextmenu + .litemenu-entry:hover:not(.disabled):not(.separator) { + background-color: var(--comfy-menu-hover-bg, var(--border-color)) !important; + color: var(--fg-color); +} + +.litegraph.litecontextmenu .litemenu-entry.submenu, +.litegraph.litecontextmenu.dark .litemenu-entry.submenu { + background-color: var(--comfy-menu-bg) !important; + color: var(--input-text); +} + +.litegraph.litecontextmenu input { + background-color: var(--comfy-input-bg) !important; + color: var(--input-text) !important; +} + +.comfy-context-menu-filter { + box-sizing: border-box; + border: 1px solid #999; + margin: 0 0 5px 5px; + width: calc(100% - 10px); +} + +.comfy-img-preview { + pointer-events: none; + overflow: hidden; + display: flex; + flex-wrap: wrap; + align-content: flex-start; + justify-content: center; +} + +.comfy-img-preview img { + object-fit: contain; + width: var(--comfy-img-preview-width); + height: var(--comfy-img-preview-height); +} + +.comfy-img-preview video { + pointer-events: auto; + object-fit: contain; + height: 100%; + width: 100%; +} + +.comfy-missing-nodes li button { + font-size: 12px; + margin-left: 5px; +} + +/* Search box */ + +.litegraph.litesearchbox { + z-index: 9999 !important; + background-color: var(--comfy-menu-bg) !important; + overflow: hidden; + display: block; +} + +.litegraph.litesearchbox input, +.litegraph.litesearchbox select { + background-color: var(--comfy-input-bg) !important; + color: var(--input-text); +} + +.litegraph.lite-search-item { + color: var(--input-text); + background-color: var(--comfy-input-bg); + filter: brightness(80%); + will-change: transform; + padding-left: 0.2em; +} + +.litegraph.lite-search-item.generic_type { + color: var(--input-text); + filter: brightness(50%); + will-change: transform; +} + +audio.comfy-audio.empty-audio-widget { + display: none; +} + +#vue-app { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Set auto complete panel's width as it is not accessible within vue-root */ +.p-autocomplete-overlay { + max-width: 25vw; +} + +.p-tree-node-content { + padding: var(--comfy-tree-explorer-item-padding) !important; +} + +/* Load3d styles */ +.comfy-load-3d, +.comfy-load-3d-animation, +.comfy-preview-3d, +.comfy-preview-3d-animation { + display: flex; + flex-direction: column; + background: transparent; + flex: 1; + position: relative; + overflow: hidden; +} + +.comfy-load-3d canvas, +.comfy-load-3d-animation canvas, +.comfy-preview-3d canvas, +.comfy-preview-3d-animation canvas, +.comfy-load-3d-viewer canvas { + display: flex; + width: 100% !important; + height: 100% !important; +} + +/* End of Load3d styles */ + +/* [Desktop] Electron window specific styles */ +.app-drag { + app-region: drag; +} + +.no-drag { + app-region: no-drag; +} + +.window-actions-spacer { + width: calc(100vw - env(titlebar-area-width, 100vw)); +} +/* End of [Desktop] Electron window specific styles */ + +.lg-node { + /* Disable text selection on all nodes */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.lg-node .lg-slot, +.lg-node .lg-widget { + transition: + opacity 0.1s ease, + font-size 0.1s ease; +} + +/* Performance optimization during canvas interaction */ +.transform-pane--interacting .lg-node * { + transition: none !important; +} + +.transform-pane--interacting .lg-node { + will-change: transform; +} + +/* START LOD specific styles */ +/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */ + +.isLOD .lg-node { + box-shadow: none; + filter: none; + backdrop-filter: none; + text-shadow: none; + -webkit-mask-image: none; + mask-image: none; + clip-path: none; + background-image: none; + text-rendering: optimizeSpeed; + border-radius: 0; + contain: layout style; + transition: none; +} + +.isLOD .lg-node-widgets { + pointer-events: none; +} + +.lod-toggle { + visibility: visible; +} + +.isLOD .lod-toggle { + visibility: hidden; +} + +.lod-fallback { + display: none; +} + +.isLOD .lod-fallback { + display: block; +} + +.isLOD .image-preview img { + image-rendering: pixelated; +} + +.isLOD .slot-dot { + border-radius: 0; +} +/* END LOD specific styles */ diff --git a/build/customIconCollection.ts b/packages/design-system/src/iconCollection.ts similarity index 96% rename from build/customIconCollection.ts rename to packages/design-system/src/iconCollection.ts index f2d823ed5..170a5465f 100644 --- a/build/customIconCollection.ts +++ b/packages/design-system/src/iconCollection.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from 'url' const fileName = fileURLToPath(import.meta.url) const dirName = dirname(fileName) -const customIconsPath = join(dirName, '..', 'src', 'assets', 'icons', 'custom') +const customIconsPath = join(dirName, 'icons') // Iconify collection structure interface IconifyIcon { diff --git a/src/assets/icons/README.md b/packages/design-system/src/icons/README.md similarity index 95% rename from src/assets/icons/README.md rename to packages/design-system/src/icons/README.md index b01a3e3ef..ba7cdb3e4 100644 --- a/src/assets/icons/README.md +++ b/packages/design-system/src/icons/README.md @@ -51,7 +51,7 @@ ComfyUI supports three types of icons that can be used throughout the interface. ```vue diff --git a/src/components/common/EditableText.spec.ts b/src/components/common/EditableText.spec.ts deleted file mode 100644 index 2e7b036b5..000000000 --- a/src/components/common/EditableText.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { mount } from '@vue/test-utils' -import PrimeVue from 'primevue/config' -import InputText from 'primevue/inputtext' -import { beforeAll, describe, expect, it } from 'vitest' -import { createApp } from 'vue' - -import EditableText from './EditableText.vue' - -describe('EditableText', () => { - beforeAll(() => { - // Create a Vue app instance for PrimeVue - const app = createApp({}) - app.use(PrimeVue) - }) - - // @ts-expect-error fixme ts strict error - const mountComponent = (props, options = {}) => { - return mount(EditableText, { - global: { - plugins: [PrimeVue], - components: { InputText } - }, - props, - ...options - }) - } - - it('renders span with modelValue when not editing', () => { - const wrapper = mountComponent({ - modelValue: 'Test Text', - isEditing: false - }) - expect(wrapper.find('span').text()).toBe('Test Text') - expect(wrapper.findComponent(InputText).exists()).toBe(false) - }) - - it('renders input with modelValue when editing', () => { - const wrapper = mountComponent({ - modelValue: 'Test Text', - isEditing: true - }) - expect(wrapper.find('span').exists()).toBe(false) - expect(wrapper.findComponent(InputText).props()['modelValue']).toBe( - 'Test Text' - ) - }) - - it('emits edit event when input is submitted', async () => { - const wrapper = mountComponent({ - modelValue: 'Test Text', - isEditing: true - }) - await wrapper.findComponent(InputText).setValue('New Text') - await wrapper.findComponent(InputText).trigger('keyup.enter') - // Blur event should have been triggered - expect(wrapper.findComponent(InputText).element).not.toBe( - document.activeElement - ) - }) - - it('finishes editing on blur', async () => { - const wrapper = mountComponent({ - modelValue: 'Test Text', - isEditing: true - }) - await wrapper.findComponent(InputText).trigger('blur') - expect(wrapper.emitted('edit')).toBeTruthy() - // @ts-expect-error fixme ts strict error - expect(wrapper.emitted('edit')[0]).toEqual(['Test Text']) - }) -}) diff --git a/src/components/common/EditableText.test.ts b/src/components/common/EditableText.test.ts new file mode 100644 index 000000000..2d31123b9 --- /dev/null +++ b/src/components/common/EditableText.test.ts @@ -0,0 +1,140 @@ +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import InputText from 'primevue/inputtext' +import { beforeAll, describe, expect, it } from 'vitest' +import { createApp } from 'vue' + +import EditableText from './EditableText.vue' + +describe('EditableText', () => { + beforeAll(() => { + // Create a Vue app instance for PrimeVue + const app = createApp({}) + app.use(PrimeVue) + }) + + // @ts-expect-error fixme ts strict error + const mountComponent = (props, options = {}) => { + return mount(EditableText, { + global: { + plugins: [PrimeVue], + components: { InputText } + }, + props, + ...options + }) + } + + it('renders span with modelValue when not editing', () => { + const wrapper = mountComponent({ + modelValue: 'Test Text', + isEditing: false + }) + expect(wrapper.find('span').text()).toBe('Test Text') + expect(wrapper.findComponent(InputText).exists()).toBe(false) + }) + + it('renders input with modelValue when editing', () => { + const wrapper = mountComponent({ + modelValue: 'Test Text', + isEditing: true + }) + expect(wrapper.find('span').exists()).toBe(false) + expect(wrapper.findComponent(InputText).props()['modelValue']).toBe( + 'Test Text' + ) + }) + + it('emits edit event when input is submitted', async () => { + const wrapper = mountComponent({ + modelValue: 'Test Text', + isEditing: true + }) + await wrapper.findComponent(InputText).setValue('New Text') + await wrapper.findComponent(InputText).trigger('keyup.enter') + // Blur event should have been triggered + expect(wrapper.findComponent(InputText).element).not.toBe( + document.activeElement + ) + }) + + it('finishes editing on blur', async () => { + const wrapper = mountComponent({ + modelValue: 'Test Text', + isEditing: true + }) + await wrapper.findComponent(InputText).trigger('blur') + expect(wrapper.emitted('edit')).toBeTruthy() + // @ts-expect-error fixme ts strict error + expect(wrapper.emitted('edit')[0]).toEqual(['Test Text']) + }) + + it('cancels editing on escape key', async () => { + const wrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + + // Change the input value + await wrapper.findComponent(InputText).setValue('Modified Text') + + // Press escape + await wrapper.findComponent(InputText).trigger('keyup.escape') + + // Should emit cancel event + expect(wrapper.emitted('cancel')).toBeTruthy() + + // Should NOT emit edit event + expect(wrapper.emitted('edit')).toBeFalsy() + + // Input value should be reset to original + expect(wrapper.findComponent(InputText).props()['modelValue']).toBe( + 'Original Text' + ) + }) + + it('does not save changes when escape is pressed and blur occurs', async () => { + const wrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + + // Change the input value + await wrapper.findComponent(InputText).setValue('Modified Text') + + // Press escape (which triggers blur internally) + await wrapper.findComponent(InputText).trigger('keyup.escape') + + // Manually trigger blur to simulate the blur that happens after escape + await wrapper.findComponent(InputText).trigger('blur') + + // Should emit cancel but not edit + expect(wrapper.emitted('cancel')).toBeTruthy() + expect(wrapper.emitted('edit')).toBeFalsy() + }) + + it('saves changes on enter but not on escape', async () => { + // Test Enter key saves changes + const enterWrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + await enterWrapper.findComponent(InputText).setValue('Saved Text') + await enterWrapper.findComponent(InputText).trigger('keyup.enter') + // Trigger blur that happens after enter + await enterWrapper.findComponent(InputText).trigger('blur') + expect(enterWrapper.emitted('edit')).toBeTruthy() + // @ts-expect-error fixme ts strict error + expect(enterWrapper.emitted('edit')[0]).toEqual(['Saved Text']) + + // Test Escape key cancels changes with a fresh wrapper + const escapeWrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + await escapeWrapper.findComponent(InputText).setValue('Cancelled Text') + await escapeWrapper.findComponent(InputText).trigger('keyup.escape') + expect(escapeWrapper.emitted('cancel')).toBeTruthy() + expect(escapeWrapper.emitted('edit')).toBeFalsy() + }) +}) diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue index 16510d3fd..625df22a0 100644 --- a/src/components/common/EditableText.vue +++ b/src/components/common/EditableText.vue @@ -7,17 +7,19 @@
@@ -27,21 +29,41 @@ import InputText from 'primevue/inputtext' import { nextTick, ref, watch } from 'vue' -const { modelValue, isEditing = false } = defineProps<{ +const { + modelValue, + isEditing = false, + inputAttrs = {} +} = defineProps<{ modelValue: string isEditing?: boolean + inputAttrs?: Record }>() -const emit = defineEmits(['update:modelValue', 'edit']) +const emit = defineEmits(['update:modelValue', 'edit', 'cancel']) const inputValue = ref(modelValue) const inputRef = ref | undefined>() +const isCanceling = ref(false) const blurInputElement = () => { // @ts-expect-error - $el is an internal property of the InputText component inputRef.value?.$el.blur() } const finishEditing = () => { - emit('edit', inputValue.value) + // Don't save if we're canceling + if (!isCanceling.value) { + emit('edit', inputValue.value) + } + isCanceling.value = false +} +const cancelEditing = () => { + // Set canceling flag to prevent blur from saving + isCanceling.value = true + // Reset to original value + inputValue.value = modelValue + // Emit cancel event + emit('cancel') + // Blur the input to exit edit mode + blurInputElement() } watch( () => isEditing, diff --git a/src/components/common/ExtensionSlot.vue b/src/components/common/ExtensionSlot.vue index e031e1c8d..6819e494e 100644 --- a/src/components/common/ExtensionSlot.vue +++ b/src/components/common/ExtensionSlot.vue @@ -17,7 +17,7 @@ diff --git a/src/components/dialog/GlobalDialog.vue b/src/components/dialog/GlobalDialog.vue index a8b60a60c..4566b0684 100644 --- a/src/components/dialog/GlobalDialog.vue +++ b/src/components/dialog/GlobalDialog.vue @@ -29,7 +29,7 @@ /> @@ -43,6 +43,8 @@ const dialogStore = useDialogStore() diff --git a/src/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue b/src/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue deleted file mode 100644 index c2573981b..000000000 --- a/src/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue deleted file mode 100644 index a4bd12c0b..000000000 --- a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/src/components/dialog/content/manager/infoPanel/InfoTabs.vue b/src/components/dialog/content/manager/infoPanel/InfoTabs.vue deleted file mode 100644 index bc96d9908..000000000 --- a/src/components/dialog/content/manager/infoPanel/InfoTabs.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/src/components/dialog/content/manager/packCard/PackCard.vue b/src/components/dialog/content/manager/packCard/PackCard.vue deleted file mode 100644 index 08caeb29c..000000000 --- a/src/components/dialog/content/manager/packCard/PackCard.vue +++ /dev/null @@ -1,165 +0,0 @@ - - - - - diff --git a/src/components/dialog/content/manager/packCard/PackCardFooter.vue b/src/components/dialog/content/manager/packCard/PackCardFooter.vue deleted file mode 100644 index 1062f2cac..000000000 --- a/src/components/dialog/content/manager/packCard/PackCardFooter.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/src/components/dialog/content/manager/packIcon/PackIcon.vue b/src/components/dialog/content/manager/packIcon/PackIcon.vue deleted file mode 100644 index 47fd826cf..000000000 --- a/src/components/dialog/content/manager/packIcon/PackIcon.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/src/components/dialog/content/manager/packIcon/PackIconStacked.vue b/src/components/dialog/content/manager/packIcon/PackIconStacked.vue deleted file mode 100644 index 3815fb36c..000000000 --- a/src/components/dialog/content/manager/packIcon/PackIconStacked.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/src/components/dialog/content/setting/AboutPanel.vue b/src/components/dialog/content/setting/AboutPanel.vue index 166da2c48..9c74f2cd7 100644 --- a/src/components/dialog/content/setting/AboutPanel.vue +++ b/src/components/dialog/content/setting/AboutPanel.vue @@ -34,7 +34,6 @@ diff --git a/src/components/dialog/content/setting/CreditsPanel.vue b/src/components/dialog/content/setting/CreditsPanel.vue index 4d6486a98..374dfbe14 100644 --- a/src/components/dialog/content/setting/CreditsPanel.vue +++ b/src/components/dialog/content/setting/CreditsPanel.vue @@ -54,7 +54,7 @@