diff --git a/.github/actions/setup-comfyui-server/action.yml b/.github/actions/setup-comfyui-server/action.yml new file mode 100644 index 000000000..d1aa1bd57 --- /dev/null +++ b/.github/actions/setup-comfyui-server/action.yml @@ -0,0 +1,55 @@ +name: Setup ComfyUI Server +description: 'Setup ComfyUI server for continuous integration (with ComfyUI_devtools node installed)' +inputs: + extra_server_params: + description: 'Additional parameters to pass to ComfyUI server' + required: false + default: '' + launch_server: + description: 'Whether to launch the server after setup' + required: false + default: 'false' +runs: + using: 'composite' + steps: + # Note: this workflow assume frontend repo is checked out and is built in ../dist + + # Checkout ComfyUI repo, install the dev_tools node and start server + - name: Checkout ComfyUI + uses: actions/checkout@v5 + with: + repository: 'comfyanonymous/ComfyUI' + path: 'ComfyUI' + + - name: Install ComfyUI_devtools from frontend repo + shell: bash + run: | + mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools + if ! cp -r ./tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/; then + echo "::error::Failed to copy ComfyUI_devtools from ./tools/devtools/" + echo "::error::This action assumes the ComfyUI_frontend repository is checked out in the current working directory." + echo "::error::Please ensure you have run 'actions/checkout@v5' before calling this action." + exit 1 + fi + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install Python requirements + shell: bash + working-directory: ComfyUI + run: | + python -m pip install --upgrade pip + pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu + pip install -r requirements.txt + pip install wait-for-it + + - name: Start ComfyUI server + if: ${{ inputs.launch_server == 'true' }} + shell: bash + working-directory: ComfyUI + run: | + python main.py --cpu --multi-user --front-end-root ../dist ${{ inputs.extra_server_params }} & + wait-for-it --service 127.0.0.1:8188 -t 600 diff --git a/.github/actions/setup-frontend/action.yml b/.github/actions/setup-frontend/action.yml index 3ebc12eb3..6787552ea 100644 --- a/.github/actions/setup-frontend/action.yml +++ b/.github/actions/setup-frontend/action.yml @@ -1,31 +1,16 @@ -name: Setup Frontend -description: 'Setup ComfyUI frontend development environment' +name: Setup ComfyUI Frontend +description: 'Install nodejs/pnpm/dependencies and optionally build ComfyUI_frontend' inputs: - extra_server_params: - description: 'Additional parameters to pass to ComfyUI server' + include_build_step: + description: 'Include the build step to build the frontend. Set to true for workflows that need a built frontend' required: false - default: '' + default: 'false' runs: using: 'composite' steps: - - name: Checkout ComfyUI - uses: actions/checkout@v4 - with: - repository: 'comfyanonymous/ComfyUI' - path: 'ComfyUI' - - - name: Checkout ComfyUI_frontend - uses: actions/checkout@v4 - with: - repository: 'Comfy-Org/ComfyUI_frontend' - path: 'ComfyUI_frontend' - - - name: Copy ComfyUI_devtools from frontend repo - shell: bash - run: | - mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools - cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/ + # Note: this workflow assume frontend repo is checked out in the root of the workspace + # Install pnpm, Node.js, build frontend - name: Install pnpm uses: pnpm/action-setup@v4 with: @@ -36,32 +21,25 @@ runs: with: node-version: 'lts/*' cache: 'pnpm' - cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml' + cache-dependency-path: './pnpm-lock.yaml' - - name: Setup Python - uses: actions/setup-python@v4 + # Restore tool caches before running any build/lint operations + - name: Restore tool output cache + uses: actions/cache/restore@v4 with: - python-version: '3.10' + path: | + ./.cache + ./tsconfig.tsbuildinfo + key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }} + restore-keys: | + tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}- + tool-cache-${{ runner.os }}- - - name: Install Python requirements + - name: Install dependencies shell: bash - working-directory: ComfyUI - run: | - python -m pip install --upgrade pip - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - pip install -r requirements.txt - pip install wait-for-it + run: pnpm install --frozen-lockfile - - name: Build & Install ComfyUI_frontend + - name: Build ComfyUI_frontend + if: ${{ inputs.include_build_step == 'true' }} shell: bash - working-directory: ComfyUI_frontend - run: | - pnpm install --frozen-lockfile - pnpm build - - - name: Start ComfyUI server - shell: bash - working-directory: ComfyUI - run: | - python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist ${{ inputs.extra_server_params }} & - wait-for-it --service 127.0.0.1:8188 -t 600 \ No newline at end of file + run: pnpm build diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yml index ddd1a7605..89629fb2c 100644 --- a/.github/actions/setup-playwright/action.yml +++ b/.github/actions/setup-playwright/action.yml @@ -6,7 +6,6 @@ runs: - name: Detect Playwright version id: detect-version shell: bash - working-directory: ComfyUI_frontend run: | PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version') echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT @@ -22,10 +21,8 @@ runs: if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' shell: bash run: pnpm exec playwright install chromium --with-deps - working-directory: ComfyUI_frontend - name: Install Playwright Browsers (operating system dependencies) if: steps.cache-playwright-browsers.outputs.cache-hit == 'true' shell: bash run: pnpm exec playwright install-deps - working-directory: ComfyUI_frontend \ No newline at end of file diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 08ad70727..76a9eb0f3 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -11,6 +11,10 @@ on: pull_request: types: [labeled] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: wait-for-ci: runs-on: ubuntu-latest @@ -73,10 +77,10 @@ jobs: with: label_trigger: "claude-review" prompt: | - Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly. - - CRITICAL: You must post individual inline comments using the gh api commands shown in the file. - DO NOT create a summary comment. + Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly. + + CRITICAL: You must post individual inline comments using the gh api commands shown in the file. + DO NOT create a summary comment. Each issue must be posted as a separate inline comment on the specific line of code. anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} claude_args: "--max-turns 256 --allowedTools 'Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch'" @@ -86,3 +90,9 @@ jobs: COMMIT_SHA: ${{ github.event.pull_request.head.sha }} BASE_SHA: ${{ github.event.pull_request.base.sha }} REPOSITORY: ${{ github.repository }} + + - name: Remove claude-review label + if: always() + run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "claude-review" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/lint-and-format.yaml index 1f20ab92e..62956cadb 100644 --- a/.github/workflows/lint-and-format.yaml +++ b/.github/workflows/lint-and-format.yaml @@ -4,6 +4,10 @@ on: pull_request: branches-ignore: [wip/*, draft/*, temp/*] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: write pull-requests: write diff --git a/.github/workflows/tests-ci.yaml b/.github/workflows/tests-ci.yaml index 1e069ea11..0d33ecf19 100644 --- a/.github/workflows/tests-ci.yaml +++ b/.github/workflows/tests-ci.yaml @@ -7,70 +7,37 @@ on: branches-ignore: [wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: setup: runs-on: ubuntu-latest outputs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - - name: Checkout ComfyUI + - name: Checkout repository uses: actions/checkout@v5 + + # Setup Test Environment, build frontend but do not start server yet + - name: Setup ComfyUI server + uses: ./.github/actions/setup-comfyui-server + - name: Setup frontend + uses: ./.github/actions/setup-frontend with: - repository: 'comfyanonymous/ComfyUI' - path: 'ComfyUI' - ref: master - - - name: Checkout ComfyUI_frontend - uses: actions/checkout@v5 - with: - repository: 'Comfy-Org/ComfyUI_frontend' - path: 'ComfyUI_frontend' - - - name: Copy ComfyUI_devtools from frontend repo - run: | - mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools - cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/ - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: 'pnpm' - cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml' - - - name: Cache tool outputs - uses: actions/cache@v4 - with: - path: | - ComfyUI_frontend/.cache - ComfyUI_frontend/tsconfig.tsbuildinfo - key: playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ hashFiles('ComfyUI_frontend/src/**/*.{ts,vue,js}', 'ComfyUI_frontend/*.config.*') }} - restore-keys: | - playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}- - playwright-setup-cache-${{ runner.os }}- - playwright-tools-cache-${{ runner.os }}- - - - name: Build ComfyUI_frontend - run: | - pnpm install --frozen-lockfile - pnpm build - working-directory: ComfyUI_frontend + include_build_step: true + - name: Setup Playwright + uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers + # Save the entire workspace as cache for later test jobs to restore - name: Generate cache key id: cache-key run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT - - - name: Save cache uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 with: - path: | - ComfyUI - ComfyUI_frontend + path: . key: comfyui-setup-${{ steps.cache-key.outputs.key }} # Sharded chromium tests @@ -85,54 +52,35 @@ jobs: shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] shardTotal: [8] steps: + # download built frontend repo from setup job - name: Wait for cache propagation run: sleep 10 - - name: Restore cached setup - uses: actions/cache/restore@v4 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 with: fail-on-cache-miss: true - path: | - ComfyUI - ComfyUI_frontend + path: . key: comfyui-setup-${{ needs.setup.outputs.cache-key }} - - name: Install pnpm - uses: pnpm/action-setup@v4 + # Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job + - name: Setup ComfyUI server + uses: ./.github/actions/setup-comfyui-server with: - version: 10 - - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - cache: 'pip' - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - pip install -r requirements.txt - pip install wait-for-it - working-directory: ComfyUI - - + launch_server: true + - name: Setup nodejs, pnpm, reuse built frontend + uses: ./.github/actions/setup-frontend - name: Setup Playwright - uses: ./ComfyUI_frontend/.github/actions/setup-playwright - - - name: Start ComfyUI server - run: | - python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist & - wait-for-it --service 127.0.0.1:8188 -t 600 - working-directory: ComfyUI + uses: ./.github/actions/setup-playwright + # Run sharded tests and upload sharded reports - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) id: playwright run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob - working-directory: ComfyUI_frontend env: - PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report + PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report - - uses: actions/upload-artifact@v4 + - name: Upload blob report + uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: blob-report-chromium-${{ matrix.shardIndex }} @@ -151,45 +99,27 @@ jobs: matrix: browser: [chromium-2x, chromium-0.5x, mobile-chrome] steps: + # download built frontend repo from setup job - name: Wait for cache propagation run: sleep 10 - - name: Restore cached setup - uses: actions/cache/restore@v4 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 with: fail-on-cache-miss: true - path: | - ComfyUI - ComfyUI_frontend + path: . key: comfyui-setup-${{ needs.setup.outputs.cache-key }} - - name: Install pnpm - uses: pnpm/action-setup@v4 + # Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job + - name: Setup ComfyUI server + uses: ./.github/actions/setup-comfyui-server with: - version: 10 - - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - cache: 'pip' - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - pip install -r requirements.txt - pip install wait-for-it - working-directory: ComfyUI - + launch_server: true + - name: Setup nodejs, pnpm, reuse built frontend + uses: ./.github/actions/setup-frontend - name: Setup Playwright - uses: ./ComfyUI_frontend/.github/actions/setup-playwright - - - name: Start ComfyUI server - run: | - python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist & - wait-for-it --service 127.0.0.1:8188 -t 600 - working-directory: ComfyUI + uses: ./.github/actions/setup-playwright + # Run tests and upload reports - name: Run Playwright tests (${{ matrix.browser }}) id: playwright run: | @@ -199,13 +129,13 @@ jobs: --reporter=list \ --reporter=html \ --reporter=json - working-directory: ComfyUI_frontend - - uses: actions/upload-artifact@v4 + - name: Upload Playwright report + uses: actions/upload-artifact@v4 if: always() with: name: playwright-report-${{ matrix.browser }} - path: ComfyUI_frontend/playwright-report/ + path: ./playwright-report/ retention-days: 30 # Merge sharded test reports @@ -214,32 +144,19 @@ jobs: runs-on: ubuntu-latest if: ${{ !cancelled() }} steps: - - name: Checkout ComfyUI_frontend + - name: Checkout repository uses: actions/checkout@v5 - with: - repository: 'Comfy-Org/ComfyUI_frontend' - path: 'ComfyUI_frontend' - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: 'pnpm' - cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml' - - - name: Install dependencies - run: | - pnpm install --frozen-lockfile - working-directory: ComfyUI_frontend + # Setup Test Environment, we only need playwright to merge reports + - name: Setup frontend + uses: ./.github/actions/setup-frontend + - name: Setup Playwright + uses: ./.github/actions/setup-playwright - name: Download blob reports uses: actions/download-artifact@v4 with: - path: ComfyUI_frontend/all-blob-reports + path: ./all-blob-reports pattern: blob-report-chromium-* merge-multiple: true @@ -250,13 +167,12 @@ jobs: # Generate JSON report separately with explicit output path PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ pnpm exec playwright merge-reports --reporter=json ./all-blob-reports - working-directory: ComfyUI_frontend - name: Upload HTML report uses: actions/upload-artifact@v4 with: name: playwright-report-chromium - path: ComfyUI_frontend/playwright-report/ + path: ./playwright-report/ retention-days: 30 #### BEGIN Deployment and commenting (non-forked PRs only) @@ -272,11 +188,11 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v5 - + - name: Get start time id: start-time run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT - + - name: Post starting comment env: GITHUB_TOKEN: ${{ github.token }} @@ -287,7 +203,7 @@ jobs: "${{ github.head_ref }}" \ "starting" \ "${{ steps.start-time.outputs.time }}" - + # Deploy and comment for non-forked PRs only deploy-and-comment: needs: [playwright-tests, merge-reports] @@ -299,23 +215,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v5 - + - name: Download all playwright reports uses: actions/download-artifact@v4 with: pattern: playwright-report-* path: reports - - - name: Make deployment script executable - run: chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh - + - name: Deploy reports and comment on PR env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_TOKEN: ${{ github.token }} run: | - ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ + bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ "${{ github.event.pull_request.number }}" \ "${{ github.head_ref }}" \ "completed" diff --git a/.github/workflows/update-locales-for-given-custom-node-repository.yaml b/.github/workflows/update-locales-for-given-custom-node-repository.yaml index ec085eab5..b9d1b33b9 100644 --- a/.github/workflows/update-locales-for-given-custom-node-repository.yaml +++ b/.github/workflows/update-locales-for-given-custom-node-repository.yaml @@ -21,90 +21,64 @@ jobs: update-locales: runs-on: ubuntu-latest steps: - - name: Checkout ComfyUI + - name: Checkout repository uses: actions/checkout@v5 + + # Setup playwright environment with custom node repository + - name: Setup ComfyUI Server (without launching) + uses: ./.github/actions/setup-comfyui-server + - name: Setup frontend + uses: ./.github/actions/setup-frontend with: - repository: comfyanonymous/ComfyUI - path: ComfyUI - ref: master - - name: Checkout ComfyUI_frontend - uses: actions/checkout@v5 - with: - repository: Comfy-Org/ComfyUI_frontend - path: ComfyUI_frontend - - name: Copy ComfyUI_devtools from frontend repo - run: | - mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools - cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/ + include_build_step: 'true' + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + + # Install the custom node repository - name: Checkout custom node repository uses: actions/checkout@v5 with: repository: ${{ inputs.owner }}/${{ inputs.repository }} path: 'ComfyUI/custom_nodes/${{ inputs.repository }}' - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Install ComfyUI requirements - run: | - python -m pip install --upgrade pip - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - pip install -r requirements.txt - pip install wait-for-it - working-directory: ComfyUI - - name: Install custom node requirements + - name: Install custom node Python requirements + working-directory: ComfyUI/custom_nodes/${{ inputs.repository }} run: | if [ -f "requirements.txt" ]; then pip install -r requirements.txt fi - working-directory: ComfyUI/custom_nodes/${{ inputs.repository }} - - name: Build & Install ComfyUI_frontend - run: | - pnpm install --frozen-lockfile - pnpm build - rm -rf ../ComfyUI/web/* - mv dist/* ../ComfyUI/web/ - working-directory: ComfyUI_frontend - - name: Start ComfyUI server - run: | - python main.py --cpu --multi-user & - wait-for-it --service 127.0.0.1:8188 -t 600 + + # Start ComfyUI Server + - name: Start ComfyUI Server + shell: bash working-directory: ComfyUI - - name: Setup Playwright - uses: ./ComfyUI_frontend/.github/actions/setup-playwright + run: | + python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} & + wait-for-it --service + - name: Start dev server # Run electron dev server as it is a superset of the web dev server # We do want electron specific UIs to be translated. run: pnpm dev:electron & - working-directory: ComfyUI_frontend + - name: Capture base i18n run: pnpm exec tsx scripts/diff-i18n capture - working-directory: ComfyUI_frontend - name: Update en.json run: pnpm collect-i18n env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - working-directory: ComfyUI_frontend - name: Update translations run: pnpm locale env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - working-directory: ComfyUI_frontend - name: Diff base vs updated i18n run: pnpm exec tsx scripts/diff-i18n diff - working-directory: ComfyUI_frontend - name: Update i18n in custom node repository run: | LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/ install -d "$LOCALE_DIR" cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR" + + # Git ops for pushing changes and creating PR - name: Check and create fork of custom node repository run: | # Try to fork the repository diff --git a/.github/workflows/update-locales.yaml b/.github/workflows/update-locales.yaml index 3bf939b23..9ffa702ca 100644 --- a/.github/workflows/update-locales.yaml +++ b/.github/workflows/update-locales.yaml @@ -14,35 +14,35 @@ jobs: if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-')) runs-on: ubuntu-latest steps: - - name: Setup Frontend + - name: Checkout repository + uses: actions/checkout@v5 + + # Setup playwright environment + - name: Setup ComfyUI Frontend uses: ./.github/actions/setup-frontend - - - name: Cache tool outputs - uses: actions/cache@v4 with: - path: | - ComfyUI_frontend/.cache - ComfyUI_frontend/.cache - key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }} - restore-keys: | - i18n-tools-cache-${{ runner.os }}- + include_build_step: true + - name: Setup ComfyUI Server + uses: ./.github/actions/setup-comfyui-server + with: + launch_server: true - name: Setup Playwright uses: ./.github/actions/setup-playwright + - name: Start dev server # Run electron dev server as it is a superset of the web dev server # We do want electron specific UIs to be translated. run: pnpm dev:electron & - working-directory: ComfyUI_frontend + + # Update locales, collect new strings and update translations using OpenAI, then commit changes - name: Update en.json run: pnpm collect-i18n env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - working-directory: ComfyUI_frontend - name: Update translations run: pnpm locale env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - working-directory: ComfyUI_frontend - name: Commit updated locales run: | git config --global user.name 'github-actions' @@ -56,4 +56,3 @@ jobs: git add src/locales/ git diff --staged --quiet || git commit -m "Update locales [skip ci]" git push origin HEAD:${{ github.head_ref }} - working-directory: ComfyUI_frontend diff --git a/.github/workflows/update-node-definitions-locales.yaml b/.github/workflows/update-node-definitions-locales.yaml index b063159dd..ce991d09e 100644 --- a/.github/workflows/update-node-definitions-locales.yaml +++ b/.github/workflows/update-node-definitions-locales.yaml @@ -13,24 +13,32 @@ jobs: update-locales: runs-on: ubuntu-latest steps: - - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3 + - name: Checkout repository + uses: actions/checkout@v5 + # Setup playwright environment + - name: Setup ComfyUI Server (and start) + uses: ./.github/actions/setup-comfyui-server + with: + launch_server: true + - name: Setup frontend + uses: ./.github/actions/setup-frontend + with: + include_build_step: true - name: Setup Playwright uses: ./.github/actions/setup-playwright + - name: Start dev server # Run electron dev server as it is a superset of the web dev server # We do want electron specific UIs to be translated. run: pnpm dev:electron & - working-directory: ComfyUI_frontend - name: Update en.json run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - working-directory: ComfyUI_frontend - name: Update translations run: pnpm locale env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - working-directory: ComfyUI_frontend - name: Create Pull Request uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e with: @@ -44,4 +52,3 @@ jobs: branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }} base: main labels: dependencies - path: ComfyUI_frontend \ No newline at end of file diff --git a/.github/workflows/update-playwright-expectations.yaml b/.github/workflows/update-playwright-expectations.yaml index cc0126b7f..61d5051d4 100644 --- a/.github/workflows/update-playwright-expectations.yaml +++ b/.github/workflows/update-playwright-expectations.yaml @@ -3,42 +3,106 @@ name: Update Playwright Expectations on: pull_request: - types: [ labeled ] + types: [labeled] + issue_comment: + types: [created] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: runs-on: ubuntu-latest - if: github.event.label.name == 'New Browser Test Expectations' + if: > + ( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) || + ( github.event.issue.pull_request && + github.event_name == 'issue_comment' && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) && + startsWith(github.event.comment.body, '/update-playwright') ) steps: - - name: Checkout workflow repo - uses: actions/checkout@v5 - - name: Setup Frontend - uses: ./.github/actions/setup-frontend - - name: Setup Playwright - uses: ./.github/actions/setup-playwright - - name: Run Playwright tests and update snapshots - id: playwright-tests - run: pnpm exec playwright test --update-snapshots - continue-on-error: true - working-directory: ComfyUI_frontend - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: ComfyUI_frontend/playwright-report/ - retention-days: 30 - - name: Debugging info - run: | - echo "Branch: ${{ github.head_ref }}" - git status - working-directory: ComfyUI_frontend - - name: Commit updated expectations - run: | - git config --global user.name 'github-actions' - git config --global user.email 'github-actions@github.com' - git fetch origin ${{ github.head_ref }} - git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }} - git add browser_tests - git commit -m "[automated] Update test expectations" - git push origin HEAD:${{ github.head_ref }} - working-directory: ComfyUI_frontend + - name: Find Update Comment + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad + id: "find-update-comment" + with: + issue-number: ${{ github.event.number || github.event.issue.number }} + comment-author: "github-actions[bot]" + body-includes: "Updating Playwright Expectations" + + - name: Add Starting Reaction + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + with: + comment-id: ${{ steps.find-update-comment.outputs.comment-id }} + issue-number: ${{ github.event.number || github.event.issue.number }} + body: | + Updating Playwright Expectations + edit-mode: replace + reactions: eyes + + - name: Get Branch SHA + id: "get-branch" + run: echo ::set-output name=branch::$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName') + env: + REPO: ${{ github.repository }} + PR_NO: ${{ github.event.number || github.event.issue.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Initial Checkout + uses: actions/checkout@v5 + with: + ref: ${{ steps.get-branch.outputs.branch }} + - name: Setup Frontend + uses: ./.github/actions/setup-frontend + with: + include_build_step: true + - name: Setup ComfyUI Server + uses: ./.github/actions/setup-comfyui-server + with: + launch_server: true + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + - name: Run Playwright tests and update snapshots + id: playwright-tests + run: pnpm exec playwright test --update-snapshots + continue-on-error: true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: ./playwright-report/ + retention-days: 30 + - name: Debugging info + run: | + echo "PR: ${{ github.event.issue.number }}" + echo "Branch: ${{ steps.get-branch.outputs.branch }}" + git status + - name: Commit updated expectations + run: | + git config --global user.name 'github-actions' + git config --global user.email 'github-actions@github.com' + git add browser_tests + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "[automated] Update test expectations" + git push origin ${{ steps.get-branch.outputs.branch }} + fi + + - name: Add Done Reaction + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + if: github.event_name == 'issue_comment' + with: + comment-id: ${{ steps.find-update-comment.outputs.comment-id }} + issue-number: ${{ github.event.number || github.event.issue.number }} + reactions: +1 + reactions-edit-mode: replace + + - name: Remove New Browser Test Expectations label + if: always() && github.event_name == 'pull_request' + run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "New Browser Test Expectations" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/vitest-tests.yaml b/.github/workflows/vitest-tests.yaml index 46155d912..394145188 100644 --- a/.github/workflows/vitest-tests.yaml +++ b/.github/workflows/vitest-tests.yaml @@ -6,6 +6,10 @@ on: pull_request: branches-ignore: [wip/*, draft/*, temp/*] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest diff --git a/.storybook/README.md b/.storybook/README.md index 0d34474ec..be5405e51 100644 --- a/.storybook/README.md +++ b/.storybook/README.md @@ -211,18 +211,17 @@ This Storybook setup includes: ## Icon Usage in Storybook -In this project, the `` syntax from unplugin-icons is not supported in Storybook. +In this project, only the `` syntax from unplugin-icons is supported in Storybook. **Example:** ```vue ``` diff --git a/.stylelintrc.json b/.stylelintrc.json index e53789123..276ac8156 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -7,15 +7,15 @@ } ], "rules": { - "import-notation": "url", + "import-notation": "string", "font-family-no-missing-generic-family-keyword": true, - "declaration-block-no-redundant-longhand-properties": true, "declaration-property-value-no-unknown": [ true, { "ignoreProperties": { "speak": ["none"], - "app-region": ["drag", "no-drag"] + "app-region": ["drag", "no-drag"], + "/^(width|height)$/": ["/^v-bind/"] } } ], @@ -35,7 +35,7 @@ "selector-max-type": 2, "declaration-block-no-duplicate-properties": true, "block-no-empty": true, - "no-descending-specificity": true, + "no-descending-specificity": null, "no-duplicate-at-import-rules": true, "at-rule-no-unknown": [ true, @@ -57,7 +57,8 @@ true, { "ignoreFunctions": [ - "theme" + "theme", + "v-bind" ] } ] diff --git a/AGENTS.md b/AGENTS.md index 59c9af1cd..0d060af1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ - Routing/i18n/entry: `src/router.ts`, `src/i18n.ts`, `src/main.ts`. - Tests: unit/component in `tests-ui/` and `src/components/**/*.{test,spec}.ts`; E2E in `browser_tests/`. - Public assets: `public/`. Build output: `dist/`. -- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.js`, `.prettierrc`. +- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.ts`, `.prettierrc`. ## Build, Test, and Development Commands - `pnpm dev`: Start Vite dev server. diff --git a/CLAUDE.md b/CLAUDE.md index 74e656f00..0b187fbfc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,6 +126,5 @@ const value = api.getServerFeature('config_name', defaultValue) // Get config - NEVER use `--no-verify` flag when committing - NEVER delete or disable tests to make them pass - NEVER circumvent quality checks -- NEVER use `dark:` 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: `
` - +- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface` +- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `
` diff --git a/CODEOWNERS b/CODEOWNERS index d3517e2ab..d754859b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,3 +54,10 @@ # Translations /src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer + +# LLM Instructions (blank on purpose) +.claude/ +.cursor/ +.cursorrules +**/AGENTS.md +**/CLAUDE.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 135a9db01..f1c9341d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -255,7 +255,7 @@ pnpm format The project supports three types of icons, all with automatic imports (no manual imports needed): 1. **PrimeIcons** - Built-in PrimeVue icons using CSS classes: `` -2. **Iconify Icons** - 200,000+ icons from various libraries: ``, `` +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 `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation. diff --git a/apps/desktop-ui/src/components/install/GpuPicker.vue b/apps/desktop-ui/src/components/install/GpuPicker.vue index e95d3204c..0a8cac0a9 100644 --- a/apps/desktop-ui/src/components/install/GpuPicker.vue +++ b/apps/desktop-ui/src/components/install/GpuPicker.vue @@ -53,7 +53,7 @@ :value="$t('install.gpuPicker.recommended')" class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]" /> - +
diff --git a/apps/desktop-ui/src/components/install/InstallLocationPicker.vue b/apps/desktop-ui/src/components/install/InstallLocationPicker.vue index 7d930d00e..6b139c1e9 100644 --- a/apps/desktop-ui/src/components/install/InstallLocationPicker.vue +++ b/apps/desktop-ui/src/components/install/InstallLocationPicker.vue @@ -286,6 +286,12 @@ const onFocus = async () => { .p-accordionheader { @apply rounded-t-xl rounded-b-none; } + + .p-accordionheader-toggle-icon { + &::before { + content: '\e902'; + } + } } .p-accordioncontent { @@ -302,13 +308,5 @@ const onFocus = async () => { content: '\e933'; } } - - .p-accordionpanel-active { - .p-accordionheader-toggle-icon { - &::before { - content: '\e902'; - } - } - } } diff --git a/apps/desktop-ui/src/views/DesktopUpdateView.vue b/apps/desktop-ui/src/views/DesktopUpdateView.vue index efaa6d1f4..1cb9135e1 100644 --- a/apps/desktop-ui/src/views/DesktopUpdateView.vue +++ b/apps/desktop-ui/src/views/DesktopUpdateView.vue @@ -65,12 +65,12 @@ onUnmounted(() => electron.Validation.dispose()) .download-bg::before { @apply m-0 absolute text-muted; - font-family: 'primeicons'; + font-family: 'primeicons', sans-serif; top: -2rem; right: 2rem; speak: none; font-style: normal; - font-weight: normal; + font-weight: 400; font-variant: normal; text-transform: none; line-height: 1; diff --git a/apps/desktop-ui/src/views/MaintenanceView.vue b/apps/desktop-ui/src/views/MaintenanceView.vue index facc08dfb..dbe1b269e 100644 --- a/apps/desktop-ui/src/views/MaintenanceView.vue +++ b/apps/desktop-ui/src/views/MaintenanceView.vue @@ -186,12 +186,12 @@ onUnmounted(() => electron.Validation.dispose()) .backspan::before { @apply m-0 absolute text-muted; - font-family: 'primeicons'; + font-family: 'primeicons', sans-serif; top: -2rem; right: -2rem; speak: none; font-style: normal; - font-weight: normal; + font-weight: 400; font-variant: normal; text-transform: none; line-height: 1; diff --git a/apps/desktop-ui/src/views/ServerStartView.vue b/apps/desktop-ui/src/views/ServerStartView.vue index dd6409991..b7d1f4eac 100644 --- a/apps/desktop-ui/src/views/ServerStartView.vue +++ b/apps/desktop-ui/src/views/ServerStartView.vue @@ -18,16 +18,16 @@ style=" background: radial-gradient( ellipse 800px 600px at center, - rgba(23, 23, 23, 0.95) 0%, - rgba(23, 23, 23, 0.93) 10%, - rgba(23, 23, 23, 0.9) 20%, - rgba(23, 23, 23, 0.85) 30%, - rgba(23, 23, 23, 0.75) 40%, - rgba(23, 23, 23, 0.6) 50%, - rgba(23, 23, 23, 0.4) 60%, - rgba(23, 23, 23, 0.2) 70%, - rgba(23, 23, 23, 0.1) 80%, - rgba(23, 23, 23, 0.05) 90%, + rgb(23 23 23 / 0.95) 0%, + rgb(23 23 23 / 0.93) 10%, + rgb(23 23 23 / 0.9) 20%, + rgb(23 23 23 / 0.85) 30%, + rgb(23 23 23 / 0.75) 40%, + rgb(23 23 23 / 0.6) 50%, + rgb(23 23 23 / 0.4) 60%, + rgb(23 23 23 / 0.2) 70%, + rgb(23 23 23 / 0.1) 80%, + rgb(23 23 23 / 0.05) 90%, transparent 100% ); " diff --git a/browser_tests/assets/vueNodes/linked-int-widget.json b/browser_tests/assets/vueNodes/linked-int-widget.json new file mode 100644 index 000000000..9aa7b0f9a --- /dev/null +++ b/browser_tests/assets/vueNodes/linked-int-widget.json @@ -0,0 +1,90 @@ +{ + "id": "95ea19ba-456c-46e8-aa40-dc3ff135b746", + "revision": 0, + "last_node_id": 11, + "last_link_id": 10, + "nodes": [ + { + "id": 10, + "type": "KSampler", + "pos": [494.3333740234375, 142.3333282470703], + "size": [444, 399], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": null + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": null + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": null + }, + { + "name": "latent_image", + "type": "LATENT", + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 10 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": null + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [67, "randomize", 20, 8, "euler", "simple", 1] + }, + { + "id": 11, + "type": "PrimitiveInt", + "pos": [24.333343505859375, 149.6666717529297], + "size": [444, 125], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [10] + } + ], + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [67, "randomize"] + } + ], + "links": [[10, 11, 0, 10, 4, "INT"]], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1, + "offset": [0, 0] + }, + "frontendVersion": "1.28.6" + }, + "version": 0.4 +} diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 19796f4c4..d46b31a98 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1,6 +1,5 @@ import type { APIRequestContext, Locator, Page } from '@playwright/test' -import { expect } from '@playwright/test' -import { test as base } from '@playwright/test' +import { test as base, expect } from '@playwright/test' import dotenv from 'dotenv' import * as fs from 'fs' @@ -130,7 +129,8 @@ export class ComfyPage { // Buttons public readonly resetViewButton: Locator - public readonly queueButton: Locator + public readonly queueButton: Locator // Run button in Legacy UI + public readonly runButton: Locator // Run button (renamed "Queue" -> "Run") // Inputs public readonly workflowUploadInput: Locator @@ -165,6 +165,9 @@ export class ComfyPage { this.widgetTextBox = page.getByPlaceholder('text').nth(1) this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) + this.runButton = page + .getByTestId('queue-button') + .getByRole('button', { name: 'Run' }) this.workflowUploadInput = page.locator('#comfy-file-input') this.visibleToasts = page.locator('.p-toast-message:visible') @@ -1086,12 +1089,6 @@ export class ComfyPage { const targetPosition = await targetSlot.getPosition() - // Debug: Log the positions we're trying to use - console.log('Drag positions:', { - source: sourcePosition, - target: targetPosition - }) - await this.dragAndDrop(sourcePosition, targetPosition) await this.nextFrame() } diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index bc4f32452..86d715bfd 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -3,6 +3,8 @@ */ import type { Locator, Page } from '@playwright/test' +import { VueNodeFixture } from './utils/vueNodeFixtures' + export class VueNodeHelpers { constructor(private page: Page) {} @@ -106,6 +108,24 @@ export class VueNodeHelpers { await this.page.keyboard.press('Backspace') } + /** + * Return a DOM-focused VueNodeFixture for the first node matching the title. + * Resolves the node id up front so subsequent interactions survive title changes. + */ + async getFixtureByTitle(title: string): Promise { + const node = this.getNodeByTitle(title).first() + await node.waitFor({ state: 'visible' }) + + const nodeId = await node.evaluate((el) => el.getAttribute('data-node-id')) + if (!nodeId) { + throw new Error( + `Vue node titled "${title}" is missing its data-node-id attribute` + ) + } + + return new VueNodeFixture(this.getNodeLocator(nodeId)) + } + /** * Wait for Vue nodes to be rendered */ @@ -119,4 +139,24 @@ export class VueNodeHelpers { await this.page.waitForSelector('[data-node-id]') } } + + /** + * Get a specific widget by node title and widget name + */ + getWidgetByName(nodeTitle: string, widgetName: string): Locator { + return this.getNodeByTitle(nodeTitle).locator( + `_vue=[widget.name="${widgetName}"]` + ) + } + + /** + * Get controls for input number widgets (increment/decrement buttons and input) + */ + getInputNumberControls(widget: Locator) { + return { + input: widget.locator('input'), + incrementButton: widget.locator('button').first(), + decrementButton: widget.locator('button').last() + } + } } diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts index 5c4541b92..fca464405 100644 --- a/browser_tests/fixtures/utils/vueNodeFixtures.ts +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -1,131 +1,66 @@ -import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' +import type { Locator } 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. - */ +/** DOM-centric helper for a single Vue-rendered node on the canvas. */ export class VueNodeFixture { - constructor( - private readonly nodeRef: NodeReference, - private readonly page: Page - ) {} + constructor(private readonly locator: Locator) {} - /** - * 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 header(): Locator { + return this.locator.locator('[data-testid^="node-header-"]') } - /** - * Get the node's title element - */ - async getTitleElement(): Promise { - const header = await this.getHeader() - return header.locator('[data-testid="node-title"]') + get title(): Locator { + return this.locator.locator('[data-testid="node-title"]') + } + + get titleInput(): Locator { + return this.locator.locator('[data-testid="node-title-input"]') + } + + get body(): Locator { + return this.locator.locator('[data-testid^="node-body-"]') + } + + get collapseButton(): Locator { + return this.locator.locator('[data-testid="node-collapse-button"]') + } + + get collapseIcon(): Locator { + return this.collapseButton.locator('i') + } + + get root(): Locator { + return this.locator } - /** - * Get the current title text - */ async getTitle(): Promise { - const titleElement = await this.getTitleElement() - return (await titleElement.textContent()) || '' + return (await this.title.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) + async setTitle(value: string): Promise { + await this.header.dblclick() + const input = this.titleInput + await expect(input).toBeVisible() + await input.fill(value) 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 this.header.dblclick() + const input = this.titleInput + await expect(input).toBeVisible() 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() + await this.collapseButton.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')) || '' + return (await this.collapseIcon.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() + boundingBox(): ReturnType { + return this.locator.boundingBox() } } 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 1c29518e2..02c2e31a2 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/minimap.spec.ts b/browser_tests/tests/minimap.spec.ts index df967d911..d1ab05fc5 100644 --- a/browser_tests/tests/minimap.spec.ts +++ b/browser_tests/tests/minimap.spec.ts @@ -66,12 +66,22 @@ test.describe('Minimap', () => { await comfyPage.nextFrame() await expect(minimapContainer).not.toBeVisible() + + // Open zoom controls dropdown again + await zoomControlsButton.click() + await comfyPage.nextFrame() + await expect(toggleButton).toContainText('Show Minimap') await toggleButton.click() await comfyPage.nextFrame() await expect(minimapContainer).toBeVisible() + + // Open zoom controls dropdown again to verify button text + await zoomControlsButton.click() + await comfyPage.nextFrame() + await expect(toggleButton).toContainText('Hide Minimap') }) 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 d32511f52..d66ea8927 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/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 2548e66ae..74c9f0b4b 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/vueNodes/groups/groups.spec.ts b/browser_tests/tests/vueNodes/groups/groups.spec.ts index a43e96a5d..fcfb0efad 100644 --- a/browser_tests/tests/vueNodes/groups/groups.spec.ts +++ b/browser_tests/tests/vueNodes/groups/groups.spec.ts @@ -8,6 +8,7 @@ const CREATE_GROUP_HOTKEY = 'Control+g' test.describe('Vue Node Groups', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true) await comfyPage.vueNodes.waitForNodes() }) @@ -15,6 +16,7 @@ test.describe('Vue Node Groups', () => { await comfyPage.page.getByText('Load Checkpoint').click() await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'vue-groups-create-group.png' ) 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 index f456f95dd..bb95e63aa 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png 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 index 7c1fa01cc..13484da06 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png 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-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 index db7cb529e..16a40d83d 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png 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-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 index 2f7ffba64..15065b8f2 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png 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 index a95f9cf19..8989dc632 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -788,4 +788,171 @@ test.describe('Vue Node Link Interaction', () => { targetSlot: 2 }) }) + + test.describe('Release actions (Shift-drop)', () => { + test('Context menu opens and endpoint is pinned on Shift-drop', async ({ + comfyPage, + comfyMouse + }) => { + await comfyPage.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'context menu' + ) + + const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + expect(samplerNode).toBeTruthy() + + const outputCenter = await getSlotCenter( + comfyPage.page, + samplerNode.id, + 0, + false + ) + + const dropPos = { x: outputCenter.x + 180, y: outputCenter.y - 140 } + + await comfyMouse.move(outputCenter) + await comfyPage.page.keyboard.down('Shift') + try { + await comfyMouse.drag(dropPos) + await comfyMouse.drop() + } finally { + await comfyPage.page.keyboard.up('Shift').catch(() => {}) + } + + // Context menu should be visible + const contextMenu = comfyPage.page.locator('.litecontextmenu') + await expect(contextMenu).toBeVisible() + + // Pinned endpoint should not change with mouse movement while menu is open + const before = await comfyPage.page.evaluate(() => { + const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos + return Array.isArray(snap) ? [snap[0], snap[1]] : null + }) + expect(before).not.toBeNull() + + // Move mouse elsewhere and verify snap position is unchanged + await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 }) + const after = await comfyPage.page.evaluate(() => { + const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos + return Array.isArray(snap) ? [snap[0], snap[1]] : null + }) + expect(after).toEqual(before) + }) + + test('Context menu -> Search pre-filters by link type and connects after selection', async ({ + comfyPage, + comfyMouse + }) => { + await comfyPage.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'context menu' + ) + await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + + const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + expect(samplerNode).toBeTruthy() + + const outputCenter = await getSlotCenter( + comfyPage.page, + samplerNode.id, + 0, + false + ) + const dropPos = { x: outputCenter.x + 200, y: outputCenter.y - 120 } + + await comfyMouse.move(outputCenter) + await comfyPage.page.keyboard.down('Shift') + try { + await comfyMouse.drag(dropPos) + await comfyMouse.drop() + } finally { + await comfyPage.page.keyboard.up('Shift').catch(() => {}) + } + + // Open Search from the context menu + await comfyPage.clickContextMenuItem('Search') + + // Search box opens with prefilled type filter based on link type (LATENT) + await expect(comfyPage.searchBox.input).toBeVisible() + const chips = comfyPage.searchBox.filterChips + // Ensure at least one filter chip exists and it matches the link type + const chipCount = await chips.count() + expect(chipCount).toBeGreaterThan(0) + await expect(chips.first()).toContainText('LATENT') + + // Choose a compatible node and verify it auto-connects + await comfyPage.searchBox.fillAndSelectFirstNode('VAEDecode') + await comfyPage.nextFrame() + + // KSampler output should now have an outgoing link + const samplerOutput = await samplerNode.getOutput(0) + expect(await samplerOutput.getLinkCount()).toBe(1) + + // One of the VAEDecode nodes should have an incoming link on input[0] + const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + let linked = false + for (const vae of vaeNodes) { + const details = await getInputLinkDetails(comfyPage.page, vae.id, 0) + if (details) { + expect(details.originId).toBe(samplerNode.id) + linked = true + break + } + } + expect(linked).toBe(true) + }) + + test('Search box opens on Shift-drop and connects after selection', async ({ + comfyPage, + comfyMouse + }) => { + await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') + + const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + expect(samplerNode).toBeTruthy() + + const outputCenter = await getSlotCenter( + comfyPage.page, + samplerNode.id, + 0, + false + ) + const dropPos = { x: outputCenter.x + 140, y: outputCenter.y - 100 } + + await comfyMouse.move(outputCenter) + await comfyPage.page.keyboard.down('Shift') + try { + await comfyMouse.drag(dropPos) + await comfyMouse.drop() + } finally { + await comfyPage.page.keyboard.up('Shift').catch(() => {}) + } + + // Search box should open directly + await expect(comfyPage.searchBox.input).toBeVisible() + await expect(comfyPage.searchBox.filterChips.first()).toContainText( + 'LATENT' + ) + + // Select a compatible node and verify connection + await comfyPage.searchBox.fillAndSelectFirstNode('VAEDecode') + await comfyPage.nextFrame() + + const samplerOutput = await samplerNode.getOutput(0) + expect(await samplerOutput.getLinkCount()).toBe(1) + + const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + let linked = false + for (const vae of vaeNodes) { + const details = await getInputLinkDetails(comfyPage.page, vae.id, 0) + if (details) { + expect(details.originId).toBe(samplerNode.id) + linked = true + break + } + } + expect(linked).toBe(true) + }) + }) }) 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 index 11259d974..ab898a31d 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png 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/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index fccd20a78..49f0ea7de 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 99012d4bb..b9ffc504e 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index 6b4030ac3..ff95fb20b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 2c163f1f3..e18081fc3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index be9d370f4..f68ed4a56 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 7ede9d449..d4ad89217 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 4b549b2e4..886b44deb 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index 6636b9672..bb87c1787 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index 172c373fc..dd727958e 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts index 3984989e1..5cd6b2fea 100644 --- a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts @@ -2,70 +2,46 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' -import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures' test.describe('Vue Nodes Renaming', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() + await comfyPage.vueNodes.waitForNodes() }) test('should display node title', async ({ comfyPage }) => { - // 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') + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.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) - + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') // 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') + await expect(await vueNode.getTitle()).toBe('My Custom Sampler') + await expect(vueNode.header).toContainText('My Custom Sampler') // Test cancel with Escape - const titleElement = await vueNode.getTitleElement() - await titleElement.dblclick() + await vueNode.title.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 vueNode.titleInput.fill('This Should Be Cancelled') + await vueNode.titleInput.press('Escape') await comfyPage.nextFrame() // Title should remain as the previously saved value - const titleAfterCancel = await vueNode.getTitle() - expect(titleAfterCancel).toBe('My Custom Sampler') + await expect(await vueNode.getTitle()).toBe('My Custom Sampler') }) test('Double click node body does not trigger edit', async ({ comfyPage }) => { - const loadCheckpointNode = - comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const loadCheckpointNode = comfyPage.vueNodes + .getNodeByTitle('Load Checkpoint') + .first() const nodeBbox = await loadCheckpointNode.boundingBox() if (!nodeBbox) throw new Error('Node not found') await loadCheckpointNode.dblclick() diff --git a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts index 2af676589..98b0a63f6 100644 --- a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts @@ -49,4 +49,44 @@ test.describe('Vue Node Selection', () => { expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0) }) } + + test('should select all nodes with ctrl+a', async ({ comfyPage }) => { + const initialCount = await comfyPage.vueNodes.getNodeCount() + expect(initialCount).toBeGreaterThan(0) + + await comfyPage.canvas.press('Control+a') + + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(initialCount) + }) + + test('should select pinned node without dragging', async ({ comfyPage }) => { + const PIN_HOTKEY = 'p' + const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' + + const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint') + await checkpointNodeHeader.click() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const pinIndicator = checkpointNode.locator(PIN_INDICATOR) + await expect(pinIndicator).toBeVisible() + + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + const initialPos = await checkpointNodeHeader.boundingBox() + if (!initialPos) throw new Error('Failed to get header position') + + await comfyPage.dragAndDrop( + { x: initialPos.x + 10, y: initialPos.y + 10 }, + { x: initialPos.x + 100, y: initialPos.y + 100 } + ) + + const finalPos = await checkpointNodeHeader.boundingBox() + if (!finalPos) throw new Error('Failed to get header position after drag') + expect(finalPos).toEqual(initialPos) + + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + }) }) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts index 74ec17cc9..2d67ca99f 100644 --- a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -20,6 +20,9 @@ test.describe('Vue Node Bypass', () => { const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-bypassed-state.png' + ) await comfyPage.page.keyboard.press(BYPASS_HOTKEY) await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png new file mode 100644 index 000000000..ed4d464d4 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts index fb5fc3c17..8e8e22995 100644 --- a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts @@ -2,7 +2,6 @@ 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 }) => { @@ -10,43 +9,50 @@ test.describe('Vue Node Collapse', () => { await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() + await comfyPage.vueNodes.waitForNodes() }) 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) + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.root).toBeVisible() // Initially should not be collapsed - expect(await node.isCollapsed()).toBe(false) - const body = await vueNode.getBody() + const body = vueNode.body await expect(body).toBeVisible() + const expandedBoundingBox = await vueNode.boundingBox() + if (!expandedBoundingBox) + throw new Error('Failed to get node bounding box before collapse') // Collapse the node await vueNode.toggleCollapse() - expect(await node.isCollapsed()).toBe(true) + await comfyPage.nextFrame() // Verify node content is hidden - const collapsedSize = await node.getSize() await expect(body).not.toBeVisible() + const collapsedBoundingBox = await vueNode.boundingBox() + if (!collapsedBoundingBox) + throw new Error('Failed to get node bounding box after collapse') + expect(collapsedBoundingBox.height).toBeLessThan(expandedBoundingBox.height) // Expand again await vueNode.toggleCollapse() - expect(await node.isCollapsed()).toBe(false) + await comfyPage.nextFrame() await expect(body).toBeVisible() // Size should be restored - const expandedSize = await node.getSize() - expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height) + const expandedBoundingBoxAfter = await vueNode.boundingBox() + if (!expandedBoundingBoxAfter) + throw new Error('Failed to get node bounding box after expand') + expect(expandedBoundingBoxAfter.height).toBeGreaterThanOrEqual( + collapsedBoundingBox.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) + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.root).toBeVisible() // Check initial expanded state icon let iconClass = await vueNode.getCollapseIconClass() @@ -66,9 +72,8 @@ test.describe('Vue Node Collapse', () => { 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) + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.root).toBeVisible() // Set custom title await vueNode.setTitle('Test Sampler') @@ -83,7 +88,6 @@ test.describe('Vue Node Collapse', () => { expect(await vueNode.getTitle()).toBe('Test Sampler') // Verify title is still displayed - const header = await vueNode.getHeader() - await expect(header).toContainText('Test Sampler') + await expect(vueNode.header).toContainText('Test Sampler') }) }) 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 index e167fe5df..fb390cbd1 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png 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 index 5eee0b056..ce629c555 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png 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 index f68a50ce6..139acf177 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png 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 index f4f8e10fe..b8c718239 100644 --- a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts @@ -3,7 +3,7 @@ import { comfyPageFixture as test } from '../../../fixtures/ComfyPage' -const ERROR_CLASS = /border-error/ +const ERROR_CLASS = /border-node-stroke-error/ test.describe('Vue Node Error', () => { test.beforeEach(async ({ comfyPage }) => { @@ -17,16 +17,21 @@ test.describe('Vue Node Error', () => { 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) }) + + test('should display error state when node causes execution error', async ({ + comfyPage + }) => { + await comfyPage.setup() + await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.runButton.click() + + const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error') + await expect(raiseErrorNode).toHaveClass(ERROR_CLASS) + }) }) 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 index 89173c640..795cc4c8d 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png 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 index e6281d325..c00eddccf 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png 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 index f05e4f68b..e2552e33b 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png 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 index 37dcfd37b..3fe656ebc 100644 --- a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -4,7 +4,7 @@ import { } from '../../../fixtures/ComfyPage' const MUTE_HOTKEY = 'Control+m' -const MUTE_CLASS = /opacity-50/ +const MUTE_OPACITY = '0.5' test.describe('Vue Node Mute', () => { test.beforeEach(async ({ comfyPage }) => { @@ -19,10 +19,11 @@ test.describe('Vue Node Mute', () => { await comfyPage.page.keyboard.press(MUTE_HOTKEY) const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') - await expect(checkpointNode).toHaveClass(MUTE_CLASS) + await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY) + await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png') await comfyPage.page.keyboard.press(MUTE_HOTKEY) - await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) + await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY) }) test('should allow toggling mute on multiple selected nodes with hotkey', async ({ @@ -35,11 +36,11 @@ test.describe('Vue Node Mute', () => { 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 expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY) + await expect(ksamplerNode).toHaveCSS('opacity', MUTE_OPACITY) await comfyPage.page.keyboard.press(MUTE_HOTKEY) - await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) - await expect(ksamplerNode).not.toHaveClass(MUTE_CLASS) + await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY) + await expect(ksamplerNode).not.toHaveCSS('opacity', MUTE_OPACITY) }) }) diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png new file mode 100644 index 000000000..272363ac8 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts new file mode 100644 index 000000000..bb956e339 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts @@ -0,0 +1,42 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.describe('Vue Integer Widget', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + }) + + test('should be disabled and not allow changing value when link connected to slot', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('vueNodes/linked-int-widget') + await comfyPage.vueNodes.waitForNodes() + + const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed') + const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget) + const initialValue = Number(await controls.input.inputValue()) + + // Verify widget is disabled when linked + await controls.incrementButton.click({ force: true }) + await expect(controls.input).toHaveValue(initialValue.toString()) + + await controls.decrementButton.click({ force: true }) + await expect(controls.input).toHaveValue(initialValue.toString()) + + await expect(seedWidget).toBeVisible() + + // Delete the node that is linked to the slot (freeing up the widget) + await comfyPage.vueNodes.getNodeByTitle('Int').click() + await comfyPage.vueNodes.deleteSelected() + + // Test widget works when unlinked + await controls.incrementButton.click() + await expect(controls.input).toHaveValue((initialValue + 1).toString()) + + await controls.decrementButton.click() + await expect(controls.input).toHaveValue(initialValue.toString()) + }) +}) diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index bce3e7dea..9f0cbc649 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts new file mode 100644 index 000000000..6f3701c12 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts @@ -0,0 +1,51 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.describe('Vue Widget Reactivity', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + test('Should display added widgets', async ({ comfyPage }) => { + const loadCheckpointNode = comfyPage.page.locator( + 'css=[data-testid="node-body-4"] > .lg-node-widgets > div' + ) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets.push(node.widgets[0]) + }) + await expect(loadCheckpointNode).toHaveCount(2) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets[2] = node.widgets[0] + }) + await expect(loadCheckpointNode).toHaveCount(3) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets.splice(0, 0, node.widgets[0]) + }) + await expect(loadCheckpointNode).toHaveCount(4) + }) + test('Should hide removed widgets', async ({ comfyPage }) => { + const loadCheckpointNode = comfyPage.page.locator( + 'css=[data-testid="node-body-3"] > .lg-node-widgets > div' + ) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.pop() + }) + await expect(loadCheckpointNode).toHaveCount(5) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.length-- + }) + await expect(loadCheckpointNode).toHaveCount(4) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.splice(0, 1) + }) + await expect(loadCheckpointNode).toHaveCount(3) + }) +}) 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 fde5a2de9..89b8ae6f3 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/eslint.config.ts b/eslint.config.ts index 96d68055c..4e19eba2e 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,38 +1,70 @@ // 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 { importX } from 'eslint-plugin-import-x' import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' import storybook from 'eslint-plugin-storybook' +import tailwind from 'eslint-plugin-tailwindcss' 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 { + configs as tseslintConfigs, + parser as tseslintParser +} from 'typescript-eslint' import vueParser from 'vue-eslint-parser' const extraFileExtensions = ['.vue'] +const commonGlobals = { + ...globals.browser, + __COMFYUI_FRONTEND_VERSION__: 'readonly' +} as const + +const settings = { + 'import/resolver': { + typescript: true, + node: true + }, + tailwindcss: { + config: `${import.meta.dirname}/packages/design-system/src/css/style.css`, + functions: ['cn', 'clsx', 'tw'] + } +} as const + +const commonParserOptions = { + parser: tseslintParser, + projectService: true, + tsConfigRootDir: import.meta.dirname, + ecmaVersion: 2020, + sourceType: 'module', + extraFileExtensions +} as const + export default defineConfig([ { ignores: [ - 'src/scripts/*', - 'src/extensions/core/*', - 'src/types/vue-shim.d.ts', - 'packages/registry-types/src/comfyRegistryTypes.ts', - 'src/types/generatedManagerTypes.ts', + '.i18nrc.cjs', + 'components.d.ts', + 'lint-staged.config.js', + 'vitest.setup.ts', '**/vite.config.*.timestamp*', - '**/vitest.config.*.timestamp*' + '**/vitest.config.*.timestamp*', + 'packages/registry-types/src/comfyRegistryTypes.ts', + 'src/extensions/core/*', + 'src/scripts/*', + 'src/types/generatedManagerTypes.ts', + 'src/types/vue-shim.d.ts' ] }, { files: ['./**/*.{ts,mts}'], + settings, languageOptions: { - globals: { - ...globals.browser, - __COMFYUI_FRONTEND_VERSION__: 'readonly' - }, + globals: commonGlobals, parserOptions: { - parser: tseslint.parser, + ...commonParserOptions, projectService: { allowDefaultProject: [ 'vite.config.mts', @@ -41,37 +73,33 @@ export default defineConfig([ 'playwright.config.ts', 'playwright.i18n.config.ts' ] - }, - tsConfigRootDir: import.meta.dirname, - ecmaVersion: 2020, - sourceType: 'module', - extraFileExtensions + } } } }, { files: ['./**/*.vue'], + settings, languageOptions: { - globals: { - ...globals.browser, - __COMFYUI_FRONTEND_VERSION__: 'readonly' - }, + globals: commonGlobals, parser: vueParser, - parserOptions: { - parser: tseslint.parser, - projectService: true, - tsConfigRootDir: import.meta.dirname, - ecmaVersion: 2020, - sourceType: 'module', - extraFileExtensions - } + parserOptions: commonParserOptions } }, pluginJs.configs.recommended, - tseslint.configs.recommended, + + tseslintConfigs.recommended, + // Difference in typecheck on CI vs Local + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Bad types in the plugin + tailwind.configs['flat/recommended'], pluginVue.configs['flat/recommended'], eslintPluginPrettierRecommended, storybook.configs['flat/recommended'], + // @ts-expect-error Bad types in the plugin + importX.flatConfigs.recommended, + // @ts-expect-error Bad types in the plugin + importX.flatConfigs.typescript, { plugins: { 'unused-imports': unusedImports, @@ -91,13 +119,18 @@ export default defineConfig([ allowInterfaces: 'always' } ], + 'import-x/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import-x/no-useless-path-segments': 'error', + 'import-x/no-relative-packages': 'error', 'unused-imports/no-unused-imports': 'error', 'no-console': ['error', { allow: ['warn', 'error'] }], + 'tailwindcss/no-custom-classname': 'off', // TODO: fix '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 + 'vue/match-component-import-name': 'error', /* Toggle on to do additional until we can clean up existing violations. 'vue/no-unused-emit-declarations': 'error', 'vue/no-unused-properties': 'error', diff --git a/knip.config.ts b/knip.config.ts index 5d975c8b4..0ed7361e2 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -44,9 +44,9 @@ const config: KnipConfig = { compilers: { // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 css: (text: string) => - [ - ...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g) - ].join('\n') + [...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g)] + .map((match) => match[0].replace(/url\(['"]?([^'"()]+)['"]?\)/, '$1')) + .join('\n') }, vite: { config: ['vite?(.*).config.mts'] diff --git a/package.json b/package.json index 171caf9f7..9076140c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.28.6", + "version": "1.29.1", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -11,7 +11,7 @@ "build:desktop": "nx build @comfyorg/desktop-ui", "build-storybook": "storybook build", "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", - "build": "pnpm typecheck && nx build", + "build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build", "collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts", "dev:desktop": "nx dev @comfyorg/desktop-ui", "dev:electron": "nx serve --config vite.electron.config.mts", @@ -35,8 +35,8 @@ "prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true", "preview": "nx preview", "storybook": "nx storybook -p 6006", - "stylelint:fix": "stylelint --cache --fix", - "stylelint": "stylelint --cache", + "stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'", + "stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'", "test:browser": "pnpm exec nx e2e", "test:unit": "nx run test", "typecheck": "vue-tsc --noEmit", @@ -57,6 +57,7 @@ "@storybook/vue3-vite": "catalog:", "@tailwindcss/vite": "catalog:", "@trivago/prettier-plugin-sort-imports": "catalog:", + "@types/eslint-plugin-tailwindcss": "catalog:", "@types/fs-extra": "catalog:", "@types/jsdom": "catalog:", "@types/node": "catalog:", @@ -66,10 +67,14 @@ "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", "@vue/test-utils": "catalog:", + "cross-env": "catalog:", "eslint": "catalog:", "eslint-config-prettier": "catalog:", + "eslint-import-resolver-typescript": "catalog:", + "eslint-plugin-import-x": "catalog:", "eslint-plugin-prettier": "catalog:", "eslint-plugin-storybook": "catalog:", + "eslint-plugin-tailwindcss": "catalog:", "eslint-plugin-unused-imports": "catalog:", "eslint-plugin-vue": "catalog:", "fs-extra": "^11.2.0", diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index fcadf31de..2c58a4976 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -9,35 +9,6 @@ @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; @@ -118,6 +89,21 @@ --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); + --color-alpha-charcoal-600-30: color-mix( + in srgb, + var(--color-charcoal-600) 30%, + transparent + ); + --color-alpha-stone-100-20: color-mix( + in srgb, + var(--color-stone-100) 20%, + transparent + ); + --color-alpha-gray-500-50: color-mix( + in srgb, + var(--color-gray-500) 50%, + transparent + ); /* PrimeVue pulled colors */ --color-muted: var(--p-text-muted-color); @@ -129,6 +115,36 @@ } :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: rgb(0 122 255 / 1); + --code-bg-color: rgb(96 165 250 / 0.2); + --code-block-bg-color: rgb(60 60 60 / 0.12); + + /* --- */ + + --accent-primary: var(--color-charcoal-700); --backdrop: var(--color-white); --dialog-surface: var(--color-neutral-200); --node-component-border: var(--color-gray-400); @@ -150,15 +166,26 @@ --node-component-tooltip-border: var(--color-sand-100); --node-component-tooltip-surface: var(--color-white); --node-component-widget-input: var(--fg-color); - --node-component-widget-input-surface: rgb(from var(--color-zinc-500) r g b / 10%); + --node-component-widget-input-surface: rgb( + from var(--color-zinc-500) r g b / 10% + ); --node-component-widget-skeleton-surface: var(--color-zinc-300); - --node-stroke: var(--color-stone-100); + --node-component-disabled: var(--color-alpha-stone-100-20); + --node-icon-disabled: var(--color-alpha-gray-500-50); + --node-stroke: var(--color-gray-400); + --node-stroke-selected: var(--color-accent-primary); + --node-stroke-error: var(--color-error); + --node-stroke-executing: var(--color-blue-100); } .dark-theme { + --accent-primary: var(--color-pure-white); --backdrop: var(--color-neutral-900); --dialog-surface: var(--color-neutral-700); --node-component-border: var(--color-stone-200); + --node-component-border-error: var(--color-danger-100); + --node-component-border-executing: var(--color-blue-500); + --node-component-border-selected: var(--color-charcoal-200); --node-component-header-icon: var(--color-slate-300); --node-component-header-surface: var(--color-charcoal-800); --node-component-outline: var(--color-white); @@ -174,7 +201,12 @@ --node-component-tooltip-border: var(--color-slate-300); --node-component-tooltip-surface: var(--color-charcoal-800); --node-component-widget-skeleton-surface: var(--color-zinc-800); - --node-stroke: var(--color-slate-100); + --node-component-disabled: var(--color-alpha-charcoal-600-30); + --node-icon-disabled: var(--color-alpha-stone-100-20); + --node-stroke: var(--color-stone-200); + --node-stroke-selected: var(--color-pure-white); + --node-stroke-error: var(--color-error); + --node-stroke-executing: var(--color-blue-100); } @theme inline { @@ -211,7 +243,12 @@ --color-node-component-widget-skeleton-surface: var( --node-component-widget-skeleton-surface ); + --color-node-component-disabled: var(--node-component-disabled); + --color-node-icon-disabled: var(--node-icon-disabled); --color-node-stroke: var(--node-stroke); + --color-node-stroke-selected: var(--node-stroke-selected); + --color-node-stroke-error: var(--node-stroke-error); + --color-node-stroke-executing: var(--node-stroke-executing); } @custom-variant dark-theme { @@ -496,7 +533,7 @@ body { /* Strong and emphasis */ .comfy-markdown-content strong { - font-weight: bold; + font-weight: 700; } .comfy-markdown-content em { @@ -507,7 +544,7 @@ body { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 100; /* Sit on top */ - padding: 30px 30px 10px 30px; + padding: 30px 30px 10px; background-color: var(--comfy-menu-bg); /* Modal background */ color: var(--error-text); box-shadow: 0 0 20px #888888; @@ -555,8 +592,8 @@ body { 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); + border-radius: 0 8px 8px; + box-shadow: 3px 3px 8px rgb(0 0 0 / 0.4); } .comfy-menu-header { @@ -574,7 +611,7 @@ body { } .comfy-menu .comfy-menu-actions button { - background-color: rgba(0, 0, 0, 0); + background-color: rgb(0 0 0 / 0); padding: 0; border: none; cursor: pointer; @@ -643,13 +680,10 @@ button.comfy-close-menu-btn { } 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; @@ -689,7 +723,7 @@ span.drag-handle::after { min-width: 160px; margin: 0; padding: 3px; - font-weight: normal; + font-weight: 400; } .comfy-list-items button { @@ -806,7 +840,7 @@ dialog { } dialog::backdrop { - background: rgba(0, 0, 0, 0.5); + background: rgb(0 0 0 / 0.5); } .comfy-dialog.comfyui-dialog.comfy-modal { @@ -1012,9 +1046,6 @@ audio.comfy-audio.empty-audio-widget { .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, @@ -1041,7 +1072,6 @@ audio.comfy-audio.empty-audio-widget { filter: none; backdrop-filter: none; text-shadow: none; - -webkit-mask-image: none; mask-image: none; clip-path: none; background-image: none; @@ -1051,6 +1081,11 @@ audio.comfy-audio.empty-audio-widget { transition: none; } +.isLOD .lg-node-header { + border-radius: 0px; + pointer-events: none; +} + .isLOD .lg-node-widgets { pointer-events: none; } diff --git a/packages/design-system/src/icons/README.md b/packages/design-system/src/icons/README.md index ba7cdb3e4..7f9665a46 100644 --- a/packages/design-system/src/icons/README.md +++ b/packages/design-system/src/icons/README.md @@ -26,9 +26,9 @@ ComfyUI supports three types of icons that can be used throughout the interface. ```vue ``` @@ -77,7 +77,7 @@ ComfyUI supports three types of icons that can be used throughout the interface. @@ -88,8 +88,8 @@ ComfyUI supports three types of icons that can be used throughout the interface. ```vue