diff --git a/.gitattributes b/.gitattributes index 39d7f722c..17591e2d4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,6 +11,7 @@ *.ts text eol=lf *.vue text eol=lf *.yaml text eol=lf +*.yml text eol=lf # Generated files packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true diff --git a/.github/actions/comment-release-links/action.yaml b/.github/actions/comment-release-links/action.yaml index a198604e9..3fc704616 100644 --- a/.github/actions/comment-release-links/action.yaml +++ b/.github/actions/comment-release-links/action.yaml @@ -104,14 +104,14 @@ runs: - name: Find existing comment id: find - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 with: issue-number: ${{ inputs.issue-number || github.event.pull_request.number }} comment-author: github-actions[bot] body-includes: ${{ steps.build.outputs.marker_search }} - name: Post or update comment - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: issue-number: ${{ inputs.issue-number || github.event.pull_request.number }} comment-id: ${{ steps.find.outputs.comment-id }} diff --git a/.github/actions/setup-comfyui-server/action.yml b/.github/actions/setup-comfyui-server/action.yaml similarity index 96% rename from .github/actions/setup-comfyui-server/action.yml rename to .github/actions/setup-comfyui-server/action.yaml index d1aa1bd57..721b47e34 100644 --- a/.github/actions/setup-comfyui-server/action.yml +++ b/.github/actions/setup-comfyui-server/action.yaml @@ -16,7 +16,7 @@ runs: # Checkout ComfyUI repo, install the dev_tools node and start server - name: Checkout ComfyUI - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'comfyanonymous/ComfyUI' path: 'ComfyUI' @@ -33,7 +33,7 @@ runs: fi - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.10' diff --git a/.github/actions/setup-frontend/action.yml b/.github/actions/setup-frontend/action.yaml similarity index 61% rename from .github/actions/setup-frontend/action.yml rename to .github/actions/setup-frontend/action.yaml index 6787552ea..c4d5d4eed 100644 --- a/.github/actions/setup-frontend/action.yml +++ b/.github/actions/setup-frontend/action.yaml @@ -12,29 +12,17 @@ runs: # Install pnpm, Node.js, build frontend - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' cache-dependency-path: './pnpm-lock.yaml' - # Restore tool caches before running any build/lint operations - - name: Restore tool output cache - uses: actions/cache/restore@v4 - with: - path: | - ./.cache - ./tsconfig.tsbuildinfo - key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }} - restore-keys: | - tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}- - tool-cache-${{ runner.os }}- - - name: Install dependencies shell: bash run: pnpm install --frozen-lockfile diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yaml similarity index 96% rename from .github/actions/setup-playwright/action.yml rename to .github/actions/setup-playwright/action.yaml index 89629fb2c..63e0c0362 100644 --- a/.github/actions/setup-playwright/action.yml +++ b/.github/actions/setup-playwright/action.yaml @@ -11,7 +11,7 @@ runs: echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT - name: Cache Playwright Browsers - uses: actions/cache@v4 + uses: actions/cache@v5 # v5.0.2 id: cache-playwright-browsers with: path: '~/.cache/ms-playwright' diff --git a/.github/actions/start-comfyui-server/action.yml b/.github/actions/start-comfyui-server/action.yaml similarity index 100% rename from .github/actions/start-comfyui-server/action.yml rename to .github/actions/start-comfyui-server/action.yaml diff --git a/.github/workflows/api-update-electron-api-types.yaml b/.github/workflows/api-update-electron-api-types.yaml index b7c5bce71..fb9398a1b 100644 --- a/.github/workflows/api-update-electron-api-types.yaml +++ b/.github/workflows/api-update-electron-api-types.yaml @@ -13,15 +13,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* cache: 'pnpm' @@ -36,7 +36,7 @@ jobs: echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}' diff --git a/.github/workflows/api-update-manager-api-types.yaml b/.github/workflows/api-update-manager-api-types.yaml index a709baecf..82b39978f 100644 --- a/.github/workflows/api-update-manager-api-types.yaml +++ b/.github/workflows/api-update-manager-api-types.yaml @@ -18,15 +18,15 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* cache: 'pnpm' @@ -35,7 +35,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Checkout ComfyUI-Manager repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: Comfy-Org/ComfyUI-Manager path: ComfyUI-Manager @@ -86,7 +86,7 @@ jobs: - name: Create Pull Request if: steps.check-changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}' diff --git a/.github/workflows/api-update-registry-api-types.yaml b/.github/workflows/api-update-registry-api-types.yaml index 5ae701dc2..41521cf94 100644 --- a/.github/workflows/api-update-registry-api-types.yaml +++ b/.github/workflows/api-update-registry-api-types.yaml @@ -17,15 +17,15 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* cache: 'pnpm' @@ -34,7 +34,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Checkout comfy-api repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: Comfy-Org/comfy-api path: comfy-api @@ -87,7 +87,7 @@ jobs: - name: Create Pull Request if: steps.check-changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}' diff --git a/.github/workflows/ci-json-validation.yaml b/.github/workflows/ci-json-validation.yaml index 9fd6f915b..20a2743d1 100644 --- a/.github/workflows/ci-json-validation.yaml +++ b/.github/workflows/ci-json-validation.yaml @@ -13,6 +13,6 @@ jobs: json-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Validate JSON syntax run: ./scripts/cicd/check-json.sh diff --git a/.github/workflows/ci-lint-format.yaml b/.github/workflows/ci-lint-format.yaml index e001dc234..df3f30c38 100644 --- a/.github/workflows/ci-lint-format.yaml +++ b/.github/workflows/ci-lint-format.yaml @@ -18,23 +18,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout PR - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }} + token: ${{ secrets.PR_GH_TOKEN }} - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Run ESLint with auto-fix run: pnpm lint:fix @@ -73,7 +63,7 @@ jobs: - name: Comment on PR about auto-fix if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository continue-on-error: true - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ @@ -86,7 +76,7 @@ jobs: - name: Comment on PR about manual fix needed if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository continue-on-error: true - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/ci-python-validation.yaml b/.github/workflows/ci-python-validation.yaml index b06296391..cf392f1bf 100644 --- a/.github/workflows/ci-python-validation.yaml +++ b/.github/workflows/ci-python-validation.yaml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/ci-size-data.yaml b/.github/workflows/ci-size-data.yaml index c88be8ad5..f56c0d17d 100644 --- a/.github/workflows/ci-size-data.yaml +++ b/.github/workflows/ci-size-data.yaml @@ -17,21 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4.1.0 - with: - version: 10 - - - name: Install Node.js - uses: actions/setup-node@v5 - with: - node-version: '24.x' - cache: pnpm - - - name: Install dependencies - run: pnpm install + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Build project run: pnpm build @@ -46,7 +35,7 @@ jobs: echo ${{ github.base_ref }} > ./temp/size/base.txt - name: Upload size data - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: size-data path: temp/size diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml index 8f039f1c4..3aaeccb30 100644 --- a/.github/workflows/ci-tests-e2e-forks.yaml +++ b/.github/workflows/ci-tests-e2e-forks.yaml @@ -31,11 +31,11 @@ jobs: echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get PR Number id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const { data: prs } = await github.rest.pulls.list({ @@ -68,7 +68,7 @@ jobs: - name: Download and Deploy Reports if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index c1e0af411..b3c4e1998 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -5,8 +5,8 @@ on: push: branches: [main, master, core/*, desktop/*] pull_request: - branches-ignore: - [wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*] + branches-ignore: [wip/*, draft/*, temp/*] + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup frontend uses: ./.github/actions/setup-frontend with: @@ -25,7 +25,7 @@ jobs: # Upload only built dist/ (containerized test jobs will pnpm install without cache) - name: Upload built frontend - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: frontend-dist path: dist/ @@ -51,9 +51,9 @@ jobs: shardTotal: [8] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download built frontend - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: frontend-dist path: dist/ @@ -72,7 +72,7 @@ jobs: PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report - name: Upload blob report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: ${{ !cancelled() }} with: name: blob-report-chromium-${{ matrix.shardIndex }} @@ -98,9 +98,9 @@ jobs: browser: [chromium-2x, chromium-0.5x, mobile-chrome] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download built frontend - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: frontend-dist path: dist/ @@ -128,7 +128,7 @@ jobs: pnpm exec playwright merge-reports --reporter=json ./blob-report - name: Upload Playwright report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: playwright-report-${{ matrix.browser }} @@ -141,16 +141,13 @@ jobs: runs-on: ubuntu-latest if: ${{ !cancelled() }} steps: - - name: Checkout repository - uses: actions/checkout@v5 - - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Download blob reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: path: ./all-blob-reports pattern: blob-report-chromium-* @@ -165,7 +162,7 @@ jobs: pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports - name: Upload HTML report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: playwright-report-chromium path: ./playwright-report/ @@ -183,7 +180,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get start time id: start-time @@ -210,10 +207,10 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download all playwright reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: playwright-report-* path: reports diff --git a/.github/workflows/ci-tests-storybook-forks.yaml b/.github/workflows/ci-tests-storybook-forks.yaml index 3012f61f2..d4f18d37b 100644 --- a/.github/workflows/ci-tests-storybook-forks.yaml +++ b/.github/workflows/ci-tests-storybook-forks.yaml @@ -31,11 +31,11 @@ jobs: echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get PR Number id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const { data: prs } = await github.rest.pulls.list({ @@ -68,7 +68,7 @@ jobs: - name: Download and Deploy Storybook if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/ci-tests-storybook.yaml b/.github/workflows/ci-tests-storybook.yaml index e900a6f0f..7a91e7a01 100644 --- a/.github/workflows/ci-tests-storybook.yaml +++ b/.github/workflows/ci-tests-storybook.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Post starting comment env: @@ -36,21 +36,10 @@ jobs: workflow-url: ${{ steps.workflow-url.outputs.url }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Build Storybook run: pnpm build-storybook @@ -69,7 +58,7 @@ jobs: - name: Upload Storybook build if: success() && github.event.pull_request.head.repo.fork == false - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: storybook-static path: storybook-static/ @@ -86,27 +75,16 @@ jobs: chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 # Required for Chromatic baseline - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Build Storybook and run Chromatic id: chromatic - uses: chromaui/action@latest + uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5 with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} buildScriptName: build-storybook @@ -136,11 +114,11 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download Storybook build if: needs.storybook-build.outputs.conclusion == 'success' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: storybook-static path: storybook-static @@ -170,7 +148,7 @@ jobs: pull-requests: write steps: - name: Update comment with Chromatic URLs - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}'; diff --git a/.github/workflows/ci-tests-unit.yaml b/.github/workflows/ci-tests-unit.yaml index c2a8c1f15..e1ba5b5d9 100644 --- a/.github/workflows/ci-tests-unit.yaml +++ b/.github/workflows/ci-tests-unit.yaml @@ -16,21 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Run Vitest tests run: pnpm test:unit diff --git a/.github/workflows/ci-validate-action-pins.yaml b/.github/workflows/ci-validate-action-pins.yaml new file mode 100644 index 000000000..3cd66cd7e --- /dev/null +++ b/.github/workflows/ci-validate-action-pins.yaml @@ -0,0 +1,21 @@ +name: Validate Action SHA Pins + +on: + pull_request: + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.pinact.yaml' + +permissions: + contents: read + +jobs: + validate-pins: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: suzuki-shunsuke/pinact-action@3d49c6412901042473ffa78becddab1aea46bbea # v1.3.1 + with: + skip_push: 'true' diff --git a/.github/workflows/ci-yaml-validation.yaml b/.github/workflows/ci-yaml-validation.yaml index 788c6b188..876fcfc4c 100644 --- a/.github/workflows/ci-yaml-validation.yaml +++ b/.github/workflows/ci-yaml-validation.yaml @@ -17,10 +17,10 @@ jobs: yaml-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/cloud-backport-tag.yaml b/.github/workflows/cloud-backport-tag.yaml index c0edec170..73b01c682 100644 --- a/.github/workflows/cloud-backport-tag.yaml +++ b/.github/workflows/cloud-backport-tag.yaml @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout merge commit - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' diff --git a/.github/workflows/i18n-update-core.yaml b/.github/workflows/i18n-update-core.yaml index bdf58b52f..5f0985b93 100644 --- a/.github/workflows/i18n-update-core.yaml +++ b/.github/workflows/i18n-update-core.yaml @@ -16,7 +16,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 + with: + token: ${{ secrets.PR_GH_TOKEN }} # Setup playwright environment - name: Setup ComfyUI Frontend diff --git a/.github/workflows/i18n-update-custom-nodes.yaml b/.github/workflows/i18n-update-custom-nodes.yaml index 5844065ad..225c1b3e3 100644 --- a/.github/workflows/i18n-update-custom-nodes.yaml +++ b/.github/workflows/i18n-update-custom-nodes.yaml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Setup playwright environment with custom node repository - name: Setup ComfyUI Server (without launching) @@ -36,7 +36,7 @@ jobs: # Install the custom node repository - name: Checkout custom node repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: ${{ inputs.owner }}/${{ inputs.repository }} path: 'ComfyUI/custom_nodes/${{ inputs.repository }}' @@ -113,7 +113,7 @@ jobs: git commit -m "Update locales" - name: Install SSH key For PUSH - uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 + uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0 with: # PR private key from action server key: ${{ secrets.PR_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/i18n-update-nodes.yaml b/.github/workflows/i18n-update-nodes.yaml index 9afc1f195..5a72e5b10 100644 --- a/.github/workflows/i18n-update-nodes.yaml +++ b/.github/workflows/i18n-update-nodes.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Setup playwright environment - name: Setup ComfyUI Server (and start) uses: ./.github/actions/setup-comfyui-server @@ -40,7 +40,7 @@ jobs: env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: 'Update locales for node definitions' diff --git a/.github/workflows/pr-backport.yaml b/.github/workflows/pr-backport.yaml index 968fcfd81..a6b15db5f 100644 --- a/.github/workflows/pr-backport.yaml +++ b/.github/workflows/pr-backport.yaml @@ -64,7 +64,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/pr-claude-review.yaml b/.github/workflows/pr-claude-review.yaml index 56fcc8c9b..b1f3d1a7f 100644 --- a/.github/workflows/pr-claude-review.yaml +++ b/.github/workflows/pr-claude-review.yaml @@ -23,18 +23,18 @@ jobs: timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: refs/pull/${{ github.event.pull_request.number }}/head - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'pnpm' @@ -44,7 +44,7 @@ jobs: pnpm install -g typescript @vue/compiler-sfc - name: Run Claude PR Review - uses: anthropics/claude-code-action@v1.0.6 + uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38 with: label_trigger: 'claude-review' prompt: | diff --git a/.github/workflows/pr-size-report.yaml b/.github/workflows/pr-size-report.yaml index 38b742054..769ce0e1a 100644 --- a/.github/workflows/pr-size-report.yaml +++ b/.github/workflows/pr-size-report.yaml @@ -33,24 +33,13 @@ jobs: github.event_name == 'workflow_dispatch' ) steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4.1.0 - with: - version: 10 - - - name: Install Node.js - uses: actions/setup-node@v5 - with: - node-version: '24.x' - cache: pnpm - - - name: Install dependencies - run: pnpm install + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Download size data - uses: dawidd6/action-download-artifact@v11 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 with: name: size-data run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }} @@ -75,7 +64,7 @@ jobs: fi - name: Download previous size data - uses: dawidd6/action-download-artifact@v11 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 with: branch: ${{ steps.pr-base.outputs.content }} workflow: ci-size-data.yaml @@ -89,12 +78,12 @@ jobs: - name: Read size report id: size-report - uses: juliangruber/read-file-action@v1 + uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7 with: path: ./size-report.md - name: Create or update PR comment - uses: actions-cool/maintain-one-comment@v3 + uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 with: token: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr-number.outputs.content }} diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index 628bc3039..1372c8126 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -38,7 +38,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Find Update Comment - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 id: 'find-update-comment' with: issue-number: ${{ steps.pr-info.outputs.pr-number }} @@ -46,7 +46,7 @@ jobs: body-includes: 'Updating Playwright Expectations' - name: Add Starting Reaction - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: comment-id: ${{ steps.find-update-comment.outputs.comment-id }} issue-number: ${{ steps.pr-info.outputs.pr-number }} @@ -56,7 +56,7 @@ jobs: reactions: eyes - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.pr-info.outputs.branch }} - name: Setup frontend @@ -66,7 +66,7 @@ jobs: # Upload built dist/ (containerized test jobs will pnpm install without cache) - name: Upload built frontend - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: frontend-dist path: dist/ @@ -91,11 +91,11 @@ jobs: shardTotal: [4] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ needs.setup.outputs.branch }} - name: Download built frontend - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: frontend-dist path: dist/ @@ -149,7 +149,7 @@ jobs: # Upload ONLY the changed files from this shard - name: Upload changed snapshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: steps.changed-snapshots.outputs.has-changes == 'true' with: name: snapshots-shard-${{ matrix.shardIndex }} @@ -157,7 +157,7 @@ jobs: retention-days: 1 - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: playwright-report-shard-${{ matrix.shardIndex }} @@ -170,17 +170,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ needs.setup.outputs.branch }} # Download all changed snapshot files from shards - name: Download snapshot artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: snapshots-shard-* path: ./downloaded-snapshots - merge-multiple: false + merge-multiple: true - name: List downloaded files run: | @@ -206,13 +206,13 @@ jobs: echo "MERGING CHANGED SNAPSHOTS" echo "==========================================" - # Check if any artifacts were downloaded + # Check if any artifacts were downloaded (merge-multiple puts files directly in path) if [ ! -d "./downloaded-snapshots" ]; then echo "No snapshot artifacts to merge" echo "==========================================" echo "MERGE COMPLETE" echo "==========================================" - echo "Shards merged: 0" + echo "Files merged: 0" exit 0 fi @@ -222,37 +222,29 @@ jobs: exit 1 fi - merged_count=0 + # Count files to merge + file_count=$(find ./downloaded-snapshots -type f | wc -l) - # For each shard's changed files, copy them directly - for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do - if [ ! -d "$shard_dir" ]; then - continue - fi + if [ "$file_count" -eq 0 ]; then + echo "No snapshot files found in downloaded artifacts" + echo "==========================================" + echo "MERGE COMPLETE" + echo "==========================================" + echo "Files merged: 0" + exit 0 + fi - shard_name=$(basename "$shard_dir") - file_count=$(find "$shard_dir" -type f | wc -l) + echo "Merging $file_count snapshot file(s)..." - if [ "$file_count" -eq 0 ]; then - echo " $shard_name: no files" - continue - fi - - echo "Processing $shard_name ($file_count file(s))..." - - # Copy files directly, preserving directory structure - # Since files are already in correct structure (no browser_tests/ prefix), just copy them all - cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /' - - merged_count=$((merged_count + 1)) - echo " ✓ Merged" - echo "" - done + # Copy all files directly, preserving directory structure + # With merge-multiple: true, files are directly in ./downloaded-snapshots/ without shard subdirs + cp -v -r ./downloaded-snapshots/* browser_tests/ 2>&1 | sed 's/^/ /' + echo "" echo "==========================================" echo "MERGE COMPLETE" echo "==========================================" - echo "Shards merged: $merged_count" + echo "Files merged: $file_count" - name: Show changes run: | @@ -301,7 +293,7 @@ jobs: echo "✓ Commit and push successful" - name: Add Done Reaction - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true' with: comment-id: ${{ needs.setup.outputs.comment-id }} diff --git a/.github/workflows/publish-desktop-ui-on-merge.yaml b/.github/workflows/publish-desktop-ui-on-merge.yaml index 253f73cab..5036bc97b 100644 --- a/.github/workflows/publish-desktop-ui-on-merge.yaml +++ b/.github/workflows/publish-desktop-ui-on-merge.yaml @@ -20,13 +20,13 @@ jobs: dist_tag: ${{ steps.dist.outputs.dist_tag }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24.x' @@ -71,7 +71,7 @@ jobs: pull-requests: write steps: - name: Checkout merge commit - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 2 diff --git a/.github/workflows/publish-desktop-ui.yaml b/.github/workflows/publish-desktop-ui.yaml index d2741d792..2a40445a5 100644 --- a/.github/workflows/publish-desktop-ui.yaml +++ b/.github/workflows/publish-desktop-ui.yaml @@ -77,19 +77,19 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.resolve_ref.outputs.ref }} fetch-depth: 1 persist-credentials: false - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24.x' cache: 'pnpm' diff --git a/.github/workflows/release-biweekly-comfyui.yaml b/.github/workflows/release-biweekly-comfyui.yaml index 8c75548ce..be25e5ed7 100644 --- a/.github/workflows/release-biweekly-comfyui.yaml +++ b/.github/workflows/release-biweekly-comfyui.yaml @@ -61,13 +61,13 @@ jobs: steps: - name: Checkout ComfyUI_frontend - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 path: frontend - name: Checkout ComfyUI (sparse) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: Comfy-Org/ComfyUI sparse-checkout: | @@ -75,12 +75,12 @@ jobs: path: comfyui - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* @@ -169,7 +169,7 @@ jobs: steps: - name: Checkout ComfyUI fork - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }} token: ${{ secrets.PR_GH_TOKEN }} diff --git a/.github/workflows/release-branch-create.yaml b/.github/workflows/release-branch-create.yaml index 3e7290fdf..3ea488fdf 100644 --- a/.github/workflows/release-branch-create.yaml +++ b/.github/workflows/release-branch-create.yaml @@ -18,13 +18,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 'lts/*' diff --git a/.github/workflows/release-draft-create.yaml b/.github/workflows/release-draft-create.yaml index 0bcb29159..74d6c9898 100644 --- a/.github/workflows/release-draft-create.yaml +++ b/.github/workflows/release-draft-create.yaml @@ -19,12 +19,12 @@ jobs: is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' @@ -50,12 +50,13 @@ jobs: ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} ENABLE_MINIFY: 'true' USE_PROD_CONFIG: 'true' + IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }} run: | pnpm install --frozen-lockfile pnpm build pnpm zipdist - name: Upload dist artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist-files path: | @@ -66,16 +67,13 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v5 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: dist-files - name: Create release id: create_release - uses: >- - softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -98,13 +96,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: dist-files - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install build dependencies @@ -119,8 +117,7 @@ jobs: env: COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }} - name: Publish pypi package - uses: >- - pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: password: ${{ secrets.PYPI_TOKEN }} packages-dir: comfyui_frontend_package/dist @@ -147,7 +144,7 @@ jobs: pull-requests: write steps: - name: Checkout merge commit - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 2 diff --git a/.github/workflows/release-npm-types.yaml b/.github/workflows/release-npm-types.yaml index 23f0cc016..21614c8a4 100644 --- a/.github/workflows/release-npm-types.yaml +++ b/.github/workflows/release-npm-types.yaml @@ -69,18 +69,18 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.resolve_ref.outputs.ref }} fetch-depth: 1 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' diff --git a/.github/workflows/release-pypi-dev.yaml b/.github/workflows/release-pypi-dev.yaml index 868321759..5b102dbe8 100644 --- a/.github/workflows/release-pypi-dev.yaml +++ b/.github/workflows/release-pypi-dev.yaml @@ -15,12 +15,12 @@ jobs: version: ${{ steps.current_version.outputs.version }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' @@ -40,7 +40,7 @@ jobs: pnpm build pnpm zipdist - name: Upload dist artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist-files path: | @@ -52,13 +52,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: dist-files - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install build dependencies @@ -73,7 +73,7 @@ jobs: env: COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }} - name: Publish pypi package - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: password: ${{ secrets.PYPI_TOKEN }} packages-dir: comfyui_frontend_package/dist diff --git a/.github/workflows/release-version-bump.yaml b/.github/workflows/release-version-bump.yaml index 4f0d033d9..d7ba7358e 100644 --- a/.github/workflows/release-version-bump.yaml +++ b/.github/workflows/release-version-bump.yaml @@ -65,7 +65,7 @@ jobs: - name: Close stale nightly version bump PRs if: github.event_name == 'schedule' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ github.token }} script: | @@ -118,7 +118,7 @@ jobs: core.info(`Closed ${closed.length} stale PR(s).`) - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.prepared-inputs.outputs.branch }} fetch-depth: 0 @@ -142,12 +142,12 @@ jobs: echo "✅ Branch '$BRANCH' exists" - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* @@ -180,7 +180,7 @@ jobs: echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT" - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}' diff --git a/.github/workflows/version-bump-desktop-ui.yaml b/.github/workflows/version-bump-desktop-ui.yaml index 0a8aca1b7..0ae940639 100644 --- a/.github/workflows/version-bump-desktop-ui.yaml +++ b/.github/workflows/version-bump-desktop-ui.yaml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.inputs.branch }} fetch-depth: 0 @@ -51,12 +51,12 @@ jobs: echo "✅ Branch '$BRANCH' exists" - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24.x' cache: 'pnpm' @@ -79,7 +79,7 @@ jobs: echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}' diff --git a/.github/workflows/weekly-docs-check.yaml b/.github/workflows/weekly-docs-check.yaml index d5c8dc51e..81519e49e 100644 --- a/.github/workflows/weekly-docs-check.yaml +++ b/.github/workflows/weekly-docs-check.yaml @@ -22,18 +22,18 @@ jobs: timeout-minutes: 45 steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: - fetch-depth: 0 + fetch-depth: 50 ref: main - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'pnpm' @@ -49,7 +49,7 @@ jobs: fi - name: Run Claude Documentation Review - uses: anthropics/claude-code-action@v1.0.6 + uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38 with: prompt: | Is all documentation still 100% accurate? @@ -130,7 +130,7 @@ jobs: - name: Create or Update Pull Request if: steps.check_changes.outputs.has_changes == 'true' - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: 'docs: weekly documentation accuracy update' diff --git a/.pinact.yaml b/.pinact.yaml new file mode 100644 index 000000000..03fade044 --- /dev/null +++ b/.pinact.yaml @@ -0,0 +1,24 @@ +# pinact configuration +# https://github.com/suzuki-shunsuke/pinact +version: 3 + +files: + - pattern: .github/workflows/*.yaml + - pattern: .github/actions/**/*.yaml + +# Actions that don't need SHA pinning (official GitHub actions are trusted) +ignore_actions: + - name: actions/cache + ref: v5 + - name: actions/checkout + ref: v6 + - name: actions/setup-node + ref: v6 + - name: actions/setup-python + ref: v6 + - name: actions/upload-artifact + ref: v6 + - name: actions/download-artifact + ref: v7 + - name: actions/github-script + ref: v8 diff --git a/.yamllint b/.yamllint index 9108997d4..3a7b25244 100644 --- a/.yamllint +++ b/.yamllint @@ -8,3 +8,6 @@ rules: line-length: disable document-start: disable truthy: disable + comments: + min-spaces-from-content: 1 + \ No newline at end of file diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index fe439af20..2d5df88b0 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -5,7 +5,7 @@ import * as fs from 'fs' import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph' import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' -import type { KeyCombo } from '../../src/schemas/keyBindingSchema' +import type { KeyCombo } from '../../src/platform/keybindings' import type { useWorkspaceStore } from '../../src/stores/workspaceStore' import { NodeBadgeMode } from '../../src/types/nodeSource' import { ComfyActionbar } from '../helpers/actionbar' diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index bd086b461..1a18577d7 100644 --- a/browser_tests/tests/actionbar.spec.ts +++ b/browser_tests/tests/actionbar.spec.ts @@ -7,7 +7,7 @@ import { webSocketFixture } from '../fixtures/ws.ts' const test = mergeTests(comfyPageFixture, webSocketFixture) -test.describe('Actionbar', () => { +test.describe('Actionbar', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') }) diff --git a/browser_tests/tests/bottomPanelShortcuts.spec.ts b/browser_tests/tests/bottomPanelShortcuts.spec.ts index 67a3670c5..354f0fb51 100644 --- a/browser_tests/tests/bottomPanelShortcuts.spec.ts +++ b/browser_tests/tests/bottomPanelShortcuts.spec.ts @@ -2,7 +2,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Bottom Panel Shortcuts', () => { +test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') }) diff --git a/browser_tests/tests/browserTabTitle.spec.ts b/browser_tests/tests/browserTabTitle.spec.ts index fd855ea4a..f3ad9515e 100644 --- a/browser_tests/tests/browserTabTitle.spec.ts +++ b/browser_tests/tests/browserTabTitle.spec.ts @@ -2,7 +2,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Browser tab title', () => { +test.describe('Browser tab title', { tag: '@smoke' }, () => { test.describe('Beta Menu', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts index 8e39154f1..e115192c9 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -19,7 +19,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Change Tracker', () => { +test.describe('Change Tracker', { tag: '@workflow' }, () => { test.describe('Undo/Redo', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') diff --git a/browser_tests/tests/colorPalette.spec.ts b/browser_tests/tests/colorPalette.spec.ts index 3bc1b9167..0118955fb 100644 --- a/browser_tests/tests/colorPalette.spec.ts +++ b/browser_tests/tests/colorPalette.spec.ts @@ -151,7 +151,7 @@ const customColorPalettes: Record = { } } -test.describe('Color Palette', () => { +test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => { test('Can show custom color palette', async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes) // Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly @@ -194,104 +194,110 @@ test.describe('Color Palette', () => { }) }) -test.describe('Node Color Adjustments', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/every_node_color') - }) - - test('should adjust opacity via node opacity setting', async ({ - comfyPage - }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) - - // Drag mouse to force canvas to redraw - await comfyPage.page.mouse.move(0, 0) - - await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png') - - await comfyPage.setSetting('Comfy.Node.Opacity', 1.0) - - await comfyPage.page.mouse.move(8, 8) - await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png') - }) - - test('should persist color adjustments when changing themes', async ({ - comfyPage - }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.2) - await comfyPage.setSetting('Comfy.ColorPalette', 'arc') - await comfyPage.nextFrame() - await comfyPage.page.mouse.move(0, 0) - await expect(comfyPage.canvas).toHaveScreenshot( - 'node-opacity-0.2-arc-theme.png' - ) - }) - - test('should not serialize color adjustments in workflow', async ({ - comfyPage - }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.nextFrame() - const parsed = await ( - await comfyPage.page.waitForFunction( - () => { - const workflow = localStorage.getItem('workflow') - if (!workflow) return null - try { - const data = JSON.parse(workflow) - return Array.isArray(data?.nodes) ? data : null - } catch { - return null - } - }, - { timeout: 3000 } - ) - ).jsonValue() - expect(parsed.nodes).toBeDefined() - expect(Array.isArray(parsed.nodes)).toBe(true) - for (const node of parsed.nodes) { - if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/) - if (node.color) expect(node.color).not.toMatch(/hsla/) - } - }) - - test('should lighten node colors when switching to light theme', async ({ - comfyPage - }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png') - }) - - test.describe('Context menu color adjustments', () => { +test.describe( + 'Node Color Adjustments', + { tag: ['@screenshot', '@settings'] }, + () => { test.beforeEach(async ({ comfyPage }) => { + await comfyPage.loadWorkflow('nodes/every_node_color') + }) + + test('should adjust opacity via node opacity setting', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) + + // Drag mouse to force canvas to redraw + await comfyPage.page.mouse.move(0, 0) + + await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png') + + await comfyPage.setSetting('Comfy.Node.Opacity', 1.0) + + await comfyPage.page.mouse.move(8, 8) + await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png') + }) + + test('should persist color adjustments when changing themes', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.Node.Opacity', 0.2) + await comfyPage.setSetting('Comfy.ColorPalette', 'arc') + await comfyPage.nextFrame() + await comfyPage.page.mouse.move(0, 0) + await expect(comfyPage.canvas).toHaveScreenshot( + 'node-opacity-0.2-arc-theme.png' + ) + }) + + test('should not serialize color adjustments in workflow', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.setSetting('Comfy.Node.Opacity', 0.3) - const node = await comfyPage.getFirstNodeRef() - await node?.clickContextMenuOption('Colors') + await comfyPage.nextFrame() + const parsed = await ( + await comfyPage.page.waitForFunction( + () => { + const workflow = localStorage.getItem('workflow') + if (!workflow) return null + try { + const data = JSON.parse(workflow) + return Array.isArray(data?.nodes) ? data : null + } catch { + return null + } + }, + { timeout: 3000 } + ) + ).jsonValue() + expect(parsed.nodes).toBeDefined() + expect(Array.isArray(parsed.nodes)).toBe(true) + for (const node of parsed.nodes) { + if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/) + if (node.color) expect(node.color).not.toMatch(/hsla/) + } }) - test('should persist color adjustments when changing custom node colors', async ({ + test('should lighten node colors when switching to light theme', async ({ comfyPage }) => { - await comfyPage.page - .locator('.litemenu-entry.submenu span:has-text("red")') - .click() + await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( - 'node-opacity-0.3-color-changed.png' + 'node-lightened-colors.png' ) }) - test('should persist color adjustments when removing custom node color', async ({ - comfyPage - }) => { - await comfyPage.page - .locator('.litemenu-entry.submenu span:has-text("No color")') - .click() - await expect(comfyPage.canvas).toHaveScreenshot( - 'node-opacity-0.3-color-removed.png' - ) + test.describe('Context menu color adjustments', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.setSetting('Comfy.Node.Opacity', 0.3) + const node = await comfyPage.getFirstNodeRef() + await node?.clickContextMenuOption('Colors') + }) + + test('should persist color adjustments when changing custom node colors', async ({ + comfyPage + }) => { + await comfyPage.page + .locator('.litemenu-entry.submenu span:has-text("red")') + .click() + await expect(comfyPage.canvas).toHaveScreenshot( + 'node-opacity-0.3-color-changed.png' + ) + }) + + test('should persist color adjustments when removing custom node color', async ({ + comfyPage + }) => { + await comfyPage.page + .locator('.litemenu-entry.submenu span:has-text("No color")') + .click() + await expect(comfyPage.canvas).toHaveScreenshot( + 'node-opacity-0.3-color-removed.png' + ) + }) }) - }) -}) + } +) diff --git a/browser_tests/tests/commands.spec.ts b/browser_tests/tests/commands.spec.ts index e271f2e15..acf99d177 100644 --- a/browser_tests/tests/commands.spec.ts +++ b/browser_tests/tests/commands.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Keybindings', () => { +test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should execute command', async ({ comfyPage }) => { await comfyPage.registerCommand('TestCommand', () => { window['foo'] = true diff --git a/browser_tests/tests/copyPaste.spec.ts b/browser_tests/tests/copyPaste.spec.ts index cabb849e8..a04cc191b 100644 --- a/browser_tests/tests/copyPaste.spec.ts +++ b/browser_tests/tests/copyPaste.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Copy Paste', () => { +test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Can copy and paste node', async ({ comfyPage }) => { await comfyPage.clickEmptyLatentNode() await comfyPage.page.mouse.move(10, 10) diff --git a/browser_tests/tests/customIcons.spec.ts b/browser_tests/tests/customIcons.spec.ts index da640fd89..eb4eccfd5 100644 --- a/browser_tests/tests/customIcons.spec.ts +++ b/browser_tests/tests/customIcons.spec.ts @@ -22,7 +22,7 @@ async function verifyCustomIconSvg(iconElement: Locator) { expect(decodedSvg).toContain(" { +test.describe('Custom Icons', { tag: '@settings' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') }) diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index 081f47275..cfa1c8cd2 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,14 +1,14 @@ import type { Locator } from '@playwright/test' import { expect } from '@playwright/test' -import type { Keybinding } from '../../src/schemas/keyBindingSchema' +import type { Keybinding } from '../../src/platform/keybindings' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Load workflow warning', () => { +test.describe('Load workflow warning', { tag: '@ui' }, () => { test('Should display a warning when loading a workflow with missing nodes', async ({ comfyPage }) => { diff --git a/browser_tests/tests/domWidget.spec.ts b/browser_tests/tests/domWidget.spec.ts index 02392f51d..d0468210e 100644 --- a/browser_tests/tests/domWidget.spec.ts +++ b/browser_tests/tests/domWidget.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('DOM Widget', () => { +test.describe('DOM Widget', { tag: '@widget' }, () => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/collapsed_multiline') const textareaWidget = comfyPage.page.locator('.comfy-multiline-input') @@ -29,12 +29,16 @@ test.describe('DOM Widget', () => { await expect(lastMultiline).not.toBeVisible() }) - test('Position update when entering focus mode', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.executeCommand('Workspace.ToggleFocusMode') - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png') - }) + test( + 'Position update when entering focus mode', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.executeCommand('Workspace.ToggleFocusMode') + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png') + } + ) // No DOM widget should be created by creation of interim LGraphNode objects. test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => { diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index ba22b38e9..2173c8623 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -6,40 +6,48 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Execution', () => { - test('Report error on unconnected slot', async ({ comfyPage }) => { - await comfyPage.disconnectEdge() - await comfyPage.clickEmptySpace() +test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => { + test( + 'Report error on unconnected slot', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.disconnectEdge() + await comfyPage.clickEmptySpace() - await comfyPage.executeCommand('Comfy.QueuePrompt') - await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible() - await comfyPage.page.locator('.p-dialog-close-button').click() - await comfyPage.page.locator('.comfy-error-report').waitFor({ - state: 'hidden' - }) - await expect(comfyPage.canvas).toHaveScreenshot( - 'execution-error-unconnected-slot.png' - ) - }) + await comfyPage.executeCommand('Comfy.QueuePrompt') + await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible() + await comfyPage.page.locator('.p-dialog-close-button').click() + await comfyPage.page.locator('.comfy-error-report').waitFor({ + state: 'hidden' + }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'execution-error-unconnected-slot.png' + ) + } + ) }) -test.describe('Execute to selected output nodes', () => { - test('Execute to selected output nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('execution/partial_execution') - const input = await comfyPage.getNodeRefById(3) - const output1 = await comfyPage.getNodeRefById(1) - const output2 = await comfyPage.getNodeRefById(4) - expect(await (await input.getWidget(0)).getValue()).toBe('foo') - expect(await (await output1.getWidget(0)).getValue()).toBe('') - expect(await (await output2.getWidget(0)).getValue()).toBe('') - - await output1.click('title') - - await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') - await expect(async () => { +test.describe( + 'Execute to selected output nodes', + { tag: ['@smoke', '@workflow'] }, + () => { + test('Execute to selected output nodes', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('execution/partial_execution') + const input = await comfyPage.getNodeRefById(3) + const output1 = await comfyPage.getNodeRefById(1) + const output2 = await comfyPage.getNodeRefById(4) expect(await (await input.getWidget(0)).getValue()).toBe('foo') - expect(await (await output1.getWidget(0)).getValue()).toBe('foo') + expect(await (await output1.getWidget(0)).getValue()).toBe('') expect(await (await output2.getWidget(0)).getValue()).toBe('') - }).toPass({ timeout: 2_000 }) - }) -}) + + await output1.click('title') + + await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') + await expect(async () => { + expect(await (await input.getWidget(0)).getValue()).toBe('foo') + expect(await (await output1.getWidget(0)).getValue()).toBe('foo') + expect(await (await output2.getWidget(0)).getValue()).toBe('') + }).toPass({ timeout: 2_000 }) + }) + } +) diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 9155ae910..73f1cd2f5 100644 --- a/browser_tests/tests/featureFlags.spec.ts +++ b/browser_tests/tests/featureFlags.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Feature Flags', () => { +test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { test('Client and server exchange feature flags on connection', async ({ comfyPage }) => { diff --git a/browser_tests/tests/graph.spec.ts b/browser_tests/tests/graph.spec.ts index 0c021b5f2..340b5b697 100644 --- a/browser_tests/tests/graph.spec.ts +++ b/browser_tests/tests/graph.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Graph', () => { +test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => { // Should be able to fix link input slot index after swap the input order // Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348 test('Fix link input slots', async ({ comfyPage }) => { diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index fc8385e49..4e6d1b2fb 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Graph Canvas Menu', () => { +test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => { test.beforeEach(async ({ comfyPage }) => { // Set link render mode to spline to make sure it's not affected by other tests' // side effects. @@ -15,29 +15,33 @@ test.describe('Graph Canvas Menu', () => { await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) }) - test('Can toggle link visibility', async ({ comfyPage }) => { - const button = comfyPage.page.getByTestId('toggle-link-visibility-button') - await button.click() - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot( - 'canvas-with-hidden-links.png' - ) - const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => { - return window['LiteGraph'].HIDDEN_LINK - }) - expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe( - hiddenLinkRenderMode - ) + test( + 'Can toggle link visibility', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + const button = comfyPage.page.getByTestId('toggle-link-visibility-button') + await button.click() + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'canvas-with-hidden-links.png' + ) + const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => { + return window['LiteGraph'].HIDDEN_LINK + }) + expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe( + hiddenLinkRenderMode + ) - await button.click() - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot( - 'canvas-with-visible-links.png' - ) - expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe( - hiddenLinkRenderMode - ) - }) + await button.click() + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'canvas-with-visible-links.png' + ) + expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe( + hiddenLinkRenderMode + ) + } + ) test('Toggle minimap button is clickable and has correct test id', async ({ comfyPage diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index 9d67a0951..9dbbe58f8 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Group Node', () => { +test.describe('Group Node', { tag: '@node' }, () => { test.describe('Node library sidebar', () => { const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeCategory = 'group nodes>workflow' @@ -89,16 +89,20 @@ test.describe('Group Node', () => { // does not have a v-model on the query, so we cannot observe the raw // query update, and thus cannot set the spinning state between the raw query // update and the debounced search update. - test.skip('Can be added to canvas using search', async ({ comfyPage }) => { - const groupNodeName = 'DefautWorkflowGroupNode' - await comfyPage.convertAllNodesToGroupNode(groupNodeName) - await comfyPage.doubleClickCanvas() - await comfyPage.nextFrame() - await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName) - await expect(comfyPage.canvas).toHaveScreenshot( - 'group-node-copy-added-from-search.png' - ) - }) + test.skip( + 'Can be added to canvas using search', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + const groupNodeName = 'DefautWorkflowGroupNode' + await comfyPage.convertAllNodesToGroupNode(groupNodeName) + await comfyPage.doubleClickCanvas() + await comfyPage.nextFrame() + await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName) + await expect(comfyPage.canvas).toHaveScreenshot( + 'group-node-copy-added-from-search.png' + ) + } + ) test('Displays tooltip on title hover', async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.EnableTooltips', true) diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index 451cbffa3..a7bcc651d 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -13,7 +13,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Item Interaction', () => { +test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => { test('Can select/delete all items', async ({ comfyPage }) => { await comfyPage.loadWorkflow('groups/mixed_graph_items') await comfyPage.canvas.press('Control+a') @@ -60,13 +60,17 @@ test.describe('Node Interaction', () => { }) }) - test('@2x Can highlight selected', async ({ comfyPage }) => { - await expect(comfyPage.canvas).toHaveScreenshot('default.png') - await comfyPage.clickTextEncodeNode1() - await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png') - await comfyPage.clickTextEncodeNode2() - await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png') - }) + test( + '@2x Can highlight selected', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await expect(comfyPage.canvas).toHaveScreenshot('default.png') + await comfyPage.clickTextEncodeNode1() + await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png') + await comfyPage.clickTextEncodeNode2() + await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png') + } + ) const dragSelectNodes = async ( comfyPage: ComfyPage, @@ -150,12 +154,12 @@ test.describe('Node Interaction', () => { }) }) - test('Can drag node', async ({ comfyPage }) => { + test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.dragNode2() await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png') }) - test.describe('Edge Interaction', () => { + test.describe('Edge Interaction', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action') @@ -222,12 +226,18 @@ test.describe('Node Interaction', () => { }) }) - test('Can adjust widget value', async ({ comfyPage }) => { - await comfyPage.adjustWidgetValue() - await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png') - }) + test( + 'Can adjust widget value', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.adjustWidgetValue() + await expect(comfyPage.canvas).toHaveScreenshot( + 'adjusted-widget-value.png' + ) + } + ) - test('Link snap to slot', async ({ comfyPage }) => { + test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.loadWorkflow('links/snap_to_slot') await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png') @@ -244,57 +254,67 @@ test.describe('Node Interaction', () => { await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png') }) - test('Can batch move links by drag with shift', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/batch_move_links') - await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png') + test( + 'Can batch move links by drag with shift', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.loadWorkflow('links/batch_move_links') + await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png') - const outputSlot1Pos = { - x: 304, - y: 127 + const outputSlot1Pos = { + x: 304, + y: 127 + } + const outputSlot2Pos = { + x: 307, + y: 310 + } + + await comfyPage.page.keyboard.down('Shift') + await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos) + await comfyPage.page.keyboard.up('Shift') + + await expect(comfyPage.canvas).toHaveScreenshot( + 'batch_move_links_moved.png' + ) } - const outputSlot2Pos = { - x: 307, - y: 310 + ) + + test( + 'Can batch disconnect links with ctrl+alt+click', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + const loadCheckpointClipSlotPos = { + x: 332, + y: 508 + } + await comfyPage.canvas.click({ + modifiers: ['Control', 'Alt'], + position: loadCheckpointClipSlotPos + }) + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'batch-disconnect-links-disconnected.png' + ) } + ) - await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos) - await comfyPage.page.keyboard.up('Shift') - - await expect(comfyPage.canvas).toHaveScreenshot( - 'batch_move_links_moved.png' - ) - }) - - test('Can batch disconnect links with ctrl+alt+click', async ({ - comfyPage - }) => { - const loadCheckpointClipSlotPos = { - x: 332, - y: 508 + test( + 'Can toggle dom widget node open/closed', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await expect(comfyPage.canvas).toHaveScreenshot('default.png') + await comfyPage.clickTextEncodeNodeToggler() + await expect(comfyPage.canvas).toHaveScreenshot( + 'text-encode-toggled-off.png' + ) + await comfyPage.delay(1000) + await comfyPage.clickTextEncodeNodeToggler() + await expect(comfyPage.canvas).toHaveScreenshot( + 'text-encode-toggled-back-open.png' + ) } - await comfyPage.canvas.click({ - modifiers: ['Control', 'Alt'], - position: loadCheckpointClipSlotPos - }) - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot( - 'batch-disconnect-links-disconnected.png' - ) - }) - - test('Can toggle dom widget node open/closed', async ({ comfyPage }) => { - await expect(comfyPage.canvas).toHaveScreenshot('default.png') - await comfyPage.clickTextEncodeNodeToggler() - await expect(comfyPage.canvas).toHaveScreenshot( - 'text-encode-toggled-off.png' - ) - await comfyPage.delay(1000) - await comfyPage.clickTextEncodeNodeToggler() - await expect(comfyPage.canvas).toHaveScreenshot( - 'text-encode-toggled-back-open.png' - ) - }) + ) test('Can close prompt dialog with canvas click (number widget)', async ({ comfyPage @@ -341,19 +361,23 @@ test.describe('Node Interaction', () => { await expect(legacyPrompt).toBeHidden() }) - test('Can double click node title to edit', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - await comfyPage.canvas.dblclick({ - position: { - x: 50, - y: 10 - }, - delay: 5 - }) - await comfyPage.page.keyboard.type('Hello World') - await comfyPage.page.keyboard.press('Enter') - await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png') - }) + test( + 'Can double click node title to edit', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.canvas.dblclick({ + position: { + x: 50, + y: 10 + }, + delay: 5 + }) + await comfyPage.page.keyboard.type('Hello World') + await comfyPage.page.keyboard.press('Enter') + await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png') + } + ) test('Double click node body does not trigger edit', async ({ comfyPage @@ -369,29 +393,41 @@ test.describe('Node Interaction', () => { expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0) }) - test('Can group selected nodes', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10) - await comfyPage.select2Nodes() - await comfyPage.page.keyboard.down('Control') - await comfyPage.page.keyboard.press('KeyG') - await comfyPage.page.keyboard.up('Control') - await comfyPage.nextFrame() - // Confirm group title - await comfyPage.page.keyboard.press('Enter') - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png') - }) + test( + 'Can group selected nodes', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10) + await comfyPage.select2Nodes() + await comfyPage.page.keyboard.down('Control') + await comfyPage.page.keyboard.press('KeyG') + await comfyPage.page.keyboard.up('Control') + await comfyPage.nextFrame() + // Confirm group title + await comfyPage.page.keyboard.press('Enter') + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'group-selected-nodes.png' + ) + } + ) - test('Can fit group to contents', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/oversized_group') - await comfyPage.ctrlA() - await comfyPage.nextFrame() - await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png') - }) + test( + 'Can fit group to contents', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.loadWorkflow('groups/oversized_group') + await comfyPage.ctrlA() + await comfyPage.nextFrame() + await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'group-fit-to-contents.png' + ) + } + ) - test('Can pin/unpin nodes', async ({ comfyPage }) => { + test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.select2Nodes() await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') await comfyPage.nextFrame() @@ -401,20 +437,22 @@ test.describe('Node Interaction', () => { await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png') }) - test('Can bypass/unbypass nodes with keyboard shortcut', async ({ - comfyPage - }) => { - await comfyPage.select2Nodes() - await comfyPage.canvas.press('Control+b') - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png') - await comfyPage.canvas.press('Control+b') - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png') - }) + test( + 'Can bypass/unbypass nodes with keyboard shortcut', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.select2Nodes() + await comfyPage.canvas.press('Control+b') + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png') + await comfyPage.canvas.press('Control+b') + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png') + } + ) }) -test.describe('Group Interaction', () => { +test.describe('Group Interaction', { tag: '@screenshot' }, () => { test('Can double click group title to edit', async ({ comfyPage }) => { await comfyPage.loadWorkflow('groups/single_group') await comfyPage.canvas.dblclick({ @@ -430,7 +468,7 @@ test.describe('Group Interaction', () => { }) }) -test.describe('Canvas Interaction', () => { +test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can zoom in/out', async ({ comfyPage }) => { await comfyPage.zoom(-100) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png') @@ -632,7 +670,7 @@ test.describe('Widget Interaction', () => { }) }) -test.describe('Load workflow', () => { +test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Can load workflow with string node id', async ({ comfyPage }) => { await comfyPage.loadWorkflow('nodes/string_node_id') await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png') @@ -824,7 +862,7 @@ test.describe('Viewport settings', () => { }) }) -test.describe('Canvas Navigation', () => { +test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test.describe('Legacy Mode', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts index f4244ae66..feb3b0f5d 100644 --- a/browser_tests/tests/keybindings.spec.ts +++ b/browser_tests/tests/keybindings.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Keybindings', () => { +test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should not trigger non-modifier keybinding when typing in input fields', async ({ comfyPage }) => { diff --git a/browser_tests/tests/litegraphEvent.spec.ts b/browser_tests/tests/litegraphEvent.spec.ts index 184943fe0..12d6ec9c4 100644 --- a/browser_tests/tests/litegraphEvent.spec.ts +++ b/browser_tests/tests/litegraphEvent.spec.ts @@ -14,7 +14,7 @@ function listenForEvent(): Promise { }) } -test.describe('Canvas Event', () => { +test.describe('Canvas Event', { tag: '@canvas' }, () => { test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => { const eventPromise = comfyPage.page.evaluate(listenForEvent) const disconnectPromise = comfyPage.disconnectEdge() diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 3a7f7a71a..754f373a7 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -6,46 +6,50 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Load Workflow in Media', () => { - const fileNames = [ - 'workflow.webp', - 'edited_workflow.webp', - 'no_workflow.webp', - 'large_workflow.webp', - 'workflow_prompt_parameters.png', - 'workflow.webm', - // Skipped due to 3d widget unstable visual result. - // 3d widget shows grid after fully loaded. - // 'workflow.glb', - 'workflow.mp4', - 'workflow.mov', - 'workflow.m4v', - 'workflow.svg' - // TODO: Re-enable after fixing test asset to use core nodes only - // Currently opens missing nodes dialog which is outside scope of AVIF loading functionality - // 'workflow.avif' - ] - fileNames.forEach(async (fileName) => { - test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ - comfyPage - }) => { - await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`) - await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`) +test.describe( + 'Load Workflow in Media', + { tag: ['@screenshot', '@workflow'] }, + () => { + const fileNames = [ + 'workflow.webp', + 'edited_workflow.webp', + 'no_workflow.webp', + 'large_workflow.webp', + 'workflow_prompt_parameters.png', + 'workflow.webm', + // Skipped due to 3d widget unstable visual result. + // 3d widget shows grid after fully loaded. + // 'workflow.glb', + 'workflow.mp4', + 'workflow.mov', + 'workflow.m4v', + 'workflow.svg' + // TODO: Re-enable after fixing test asset to use core nodes only + // Currently opens missing nodes dialog which is outside scope of AVIF loading functionality + // 'workflow.avif' + ] + fileNames.forEach(async (fileName) => { + test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ + comfyPage + }) => { + await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`) + await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`) + }) }) - }) - const urls = [ - 'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png' - ] - urls.forEach(async (url) => { - test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({ - comfyPage - }) => { - await comfyPage.dragAndDropURL(url) - const readableName = url.split('/').pop() - await expect(comfyPage.canvas).toHaveScreenshot( - `dropped_workflow_url_${readableName}.png` - ) + const urls = [ + 'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png' + ] + urls.forEach(async (url) => { + test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({ + comfyPage + }) => { + await comfyPage.dragAndDropURL(url) + const readableName = url.split('/').pop() + await expect(comfyPage.canvas).toHaveScreenshot( + `dropped_workflow_url_${readableName}.png` + ) + }) }) - }) -}) + } +) diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png index 0c57f7db1..1535c58e4 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png differ diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts index 154ac3c16..e1098cd28 100644 --- a/browser_tests/tests/lodThreshold.spec.ts +++ b/browser_tests/tests/lodThreshold.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('LOD Threshold', () => { +test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { test('Should switch to low quality mode at correct zoom threshold', async ({ comfyPage }) => { @@ -149,53 +149,55 @@ test.describe('LOD Threshold', () => { expect(state.scale).toBeLessThan(0.2) // Very zoomed out }) - test('Should show visual difference between LOD on and off', async ({ - comfyPage - }) => { - // Load a workflow with text-heavy nodes for clear visual difference - await comfyPage.loadWorkflow('default') + test( + 'Should show visual difference between LOD on and off', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + // Load a workflow with text-heavy nodes for clear visual difference + await comfyPage.loadWorkflow('default') - // Set zoom level clearly below the threshold to ensure LOD activates - const targetZoom = 0.4 // Well below default threshold of ~0.571 + // Set zoom level clearly below the threshold to ensure LOD activates + const targetZoom = 0.4 // Well below default threshold of ~0.571 - // Zoom to target level - await comfyPage.page.evaluate((zoom) => { - window['app'].canvas.ds.scale = zoom - window['app'].canvas.setDirty(true, true) - }, targetZoom) - await comfyPage.nextFrame() + // Zoom to target level + await comfyPage.page.evaluate((zoom) => { + window['app'].canvas.ds.scale = zoom + window['app'].canvas.setDirty(true, true) + }, targetZoom) + await comfyPage.nextFrame() - // Take snapshot with LOD active (default 8px setting) - await expect(comfyPage.canvas).toHaveScreenshot( - 'lod-comparison-low-quality.png' - ) + // Take snapshot with LOD active (default 8px setting) + await expect(comfyPage.canvas).toHaveScreenshot( + 'lod-comparison-low-quality.png' + ) - const lowQualityState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas - return { - lowQuality: canvas.low_quality, - scale: canvas.ds.scale - } - }) - expect(lowQualityState.lowQuality).toBe(true) + const lowQualityState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + expect(lowQualityState.lowQuality).toBe(true) - // Disable LOD to see high quality at same zoom - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) - await comfyPage.nextFrame() + // Disable LOD to see high quality at same zoom + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.nextFrame() - // Take snapshot with LOD disabled (full quality at same zoom) - await expect(comfyPage.canvas).toHaveScreenshot( - 'lod-comparison-high-quality.png' - ) + // Take snapshot with LOD disabled (full quality at same zoom) + await expect(comfyPage.canvas).toHaveScreenshot( + 'lod-comparison-high-quality.png' + ) - const highQualityState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas - return { - lowQuality: canvas.low_quality, - scale: canvas.ds.scale - } - }) - expect(highQualityState.lowQuality).toBe(false) - expect(highQualityState.scale).toBeCloseTo(targetZoom, 2) - }) + const highQualityState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + expect(highQualityState.lowQuality).toBe(false) + expect(highQualityState.scale).toBeCloseTo(targetZoom, 2) + } + ) }) diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts index 1ba1e5524..06ab9a9a8 100644 --- a/browser_tests/tests/menu.spec.ts +++ b/browser_tests/tests/menu.spec.ts @@ -2,7 +2,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Menu', () => { +test.describe('Menu', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') }) diff --git a/browser_tests/tests/minimap.spec.ts b/browser_tests/tests/minimap.spec.ts index cea5fe9a8..b30adb933 100644 --- a/browser_tests/tests/minimap.spec.ts +++ b/browser_tests/tests/minimap.spec.ts @@ -2,7 +2,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Minimap', () => { +test.describe('Minimap', { tag: '@canvas' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Minimap.Visible', true) diff --git a/browser_tests/tests/mobileBaseline.spec.ts b/browser_tests/tests/mobileBaseline.spec.ts index ff766a434..9e9ddec94 100644 --- a/browser_tests/tests/mobileBaseline.spec.ts +++ b/browser_tests/tests/mobileBaseline.spec.ts @@ -1,35 +1,39 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { expect } from '@playwright/test' -test.describe('Mobile Baseline Snapshots', () => { - test('@mobile empty canvas', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ConfirmClear', false) - await comfyPage.executeCommand('Comfy.ClearWorkflow') - await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBe(0) - }).toPass({ timeout: 256 }) - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') - }) +test.describe( + 'Mobile Baseline Snapshots', + { tag: ['@mobile', '@screenshot'] }, + () => { + test('@mobile empty canvas', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.ConfirmClear', false) + await comfyPage.executeCommand('Comfy.ClearWorkflow') + await expect(async () => { + expect(await comfyPage.getGraphNodesCount()).toBe(0) + }).toPass({ timeout: 256 }) + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') + }) - test('@mobile default workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') - await expect(comfyPage.canvas).toHaveScreenshot( - 'mobile-default-workflow.png' - ) - }) + test('@mobile default workflow', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('default') + await expect(comfyPage.canvas).toHaveScreenshot( + 'mobile-default-workflow.png' + ) + }) - test('@mobile settings dialog', async ({ comfyPage }) => { - await comfyPage.settingDialog.open() - await comfyPage.nextFrame() + test('@mobile settings dialog', async ({ comfyPage }) => { + await comfyPage.settingDialog.open() + await comfyPage.nextFrame() - await expect(comfyPage.settingDialog.root).toHaveScreenshot( - 'mobile-settings-dialog.png', - { - mask: [ - comfyPage.settingDialog.root.getByTestId('current-user-indicator') - ] - } - ) - }) -}) + await expect(comfyPage.settingDialog.root).toHaveScreenshot( + 'mobile-settings-dialog.png', + { + mask: [ + comfyPage.settingDialog.root.getByTestId('current-user-indicator') + ] + } + ) + }) + } +) diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png index eaa359731..c7334eb16 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png index ff608be64..e3acabfcb 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts b/browser_tests/tests/nodeBadge.spec.ts index 111efe29c..f4cf493a9 100644 --- a/browser_tests/tests/nodeBadge.spec.ts +++ b/browser_tests/tests/nodeBadge.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Node Badge', () => { +test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add badge', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { const LGraphBadge = window['LGraphBadge'] @@ -66,50 +66,60 @@ test.describe('Node Badge', () => { }) }) -test.describe('Node source badge', () => { - Object.values(NodeBadgeMode).forEach(async (mode) => { - test(`Shows node badges (${mode})`, async ({ comfyPage }) => { - // Execution error workflow has both custom node and core node. - await comfyPage.loadWorkflow('nodes/execution_error') - await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode) - await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode) - await comfyPage.nextFrame() - await comfyPage.resetView() - await expect(comfyPage.canvas).toHaveScreenshot(`node-badge-${mode}.png`) +test.describe( + 'Node source badge', + { tag: ['@screenshot', '@smoke', '@node'] }, + () => { + Object.values(NodeBadgeMode).forEach(async (mode) => { + test(`Shows node badges (${mode})`, async ({ comfyPage }) => { + // Execution error workflow has both custom node and core node. + await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode) + await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode) + await comfyPage.nextFrame() + await comfyPage.resetView() + await expect(comfyPage.canvas).toHaveScreenshot( + `node-badge-${mode}.png` + ) + }) }) - }) -}) + } +) -test.describe('Node badge color', () => { - test('Can show node badge with unknown color palette', async ({ - comfyPage - }) => { - await comfyPage.setSetting( - 'Comfy.NodeBadge.NodeIdBadgeMode', - NodeBadgeMode.ShowAll - ) - await comfyPage.setSetting('Comfy.ColorPalette', 'unknown') - await comfyPage.nextFrame() - // Click empty space to trigger canvas re-render. - await comfyPage.clickEmptySpace() - await expect(comfyPage.canvas).toHaveScreenshot( - 'node-badge-unknown-color-palette.png' - ) - }) +test.describe( + 'Node badge color', + { tag: ['@screenshot', '@smoke', '@node'] }, + () => { + test('Can show node badge with unknown color palette', async ({ + comfyPage + }) => { + await comfyPage.setSetting( + 'Comfy.NodeBadge.NodeIdBadgeMode', + NodeBadgeMode.ShowAll + ) + await comfyPage.setSetting('Comfy.ColorPalette', 'unknown') + await comfyPage.nextFrame() + // Click empty space to trigger canvas re-render. + await comfyPage.clickEmptySpace() + await expect(comfyPage.canvas).toHaveScreenshot( + 'node-badge-unknown-color-palette.png' + ) + }) - test('Can show node badge with light color palette', async ({ - comfyPage - }) => { - await comfyPage.setSetting( - 'Comfy.NodeBadge.NodeIdBadgeMode', - NodeBadgeMode.ShowAll - ) - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.nextFrame() - // Click empty space to trigger canvas re-render. - await comfyPage.clickEmptySpace() - await expect(comfyPage.canvas).toHaveScreenshot( - 'node-badge-light-color-palette.png' - ) - }) -}) + test('Can show node badge with light color palette', async ({ + comfyPage + }) => { + await comfyPage.setSetting( + 'Comfy.NodeBadge.NodeIdBadgeMode', + NodeBadgeMode.ShowAll + ) + await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.nextFrame() + // Click empty space to trigger canvas re-render. + await comfyPage.clickEmptySpace() + await expect(comfyPage.canvas).toHaveScreenshot( + 'node-badge-light-color-palette.png' + ) + }) + } +) diff --git a/browser_tests/tests/nodeDisplay.spec.ts b/browser_tests/tests/nodeDisplay.spec.ts index fdaae14bc..541ddaf0f 100644 --- a/browser_tests/tests/nodeDisplay.spec.ts +++ b/browser_tests/tests/nodeDisplay.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => { // If an input is optional by node definition, it should be shown as // a hollow circle no matter what shape it was defined in the workflow JSON. -test.describe('Optional input', () => { +test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => { test('No shape specified', async ({ comfyPage }) => { await comfyPage.loadWorkflow('inputs/optional_input_no_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index 709c19a4b..095cb37ec 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -23,7 +23,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) { await nodeRef.click('title') } -test.describe('Node Help', () => { +test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setup() await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index 2eea22329..f227b2932 100644 --- a/browser_tests/tests/nodeSearchBox.spec.ts +++ b/browser_tests/tests/nodeSearchBox.spec.ts @@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Node search box', () => { +test.describe('Node search box', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') @@ -46,14 +46,14 @@ test.describe('Node search box', () => { await expect(comfyPage.searchBox.input).toBeVisible() }) - test('Can add node', async ({ comfyPage }) => { + test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.doubleClickCanvas() await expect(comfyPage.searchBox.input).toHaveCount(1) await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') await expect(comfyPage.canvas).toHaveScreenshot('added-node.png') }) - test('Can auto link node', async ({ comfyPage }) => { + test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.disconnectEdge() // Select the second item as the first item is always reroute await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', { @@ -62,41 +62,47 @@ test.describe('Node search box', () => { await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png') }) - test('Can auto link batch moved node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/batch_move_links') + test( + 'Can auto link batch moved node', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.loadWorkflow('links/batch_move_links') - const outputSlot1Pos = { - x: 304, - y: 127 + const outputSlot1Pos = { + x: 304, + y: 127 + } + const emptySpacePos = { + x: 5, + y: 5 + } + await comfyPage.page.keyboard.down('Shift') + await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos) + await comfyPage.page.keyboard.up('Shift') + + // Select the second item as the first item is always reroute + await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', { + suggestionIndex: 0 + }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'auto-linked-node-batch.png' + ) } - const emptySpacePos = { - x: 5, - y: 5 + ) + + test( + 'Link release connecting to node with no slots', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.disconnectEdge() + await expect(comfyPage.searchBox.input).toHaveCount(1) + await comfyPage.page.locator('.p-chip-remove-icon').click() + await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') + await expect(comfyPage.canvas).toHaveScreenshot( + 'added-node-no-connection.png' + ) } - await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos) - await comfyPage.page.keyboard.up('Shift') - - // Select the second item as the first item is always reroute - await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', { - suggestionIndex: 0 - }) - await expect(comfyPage.canvas).toHaveScreenshot( - 'auto-linked-node-batch.png' - ) - }) - - test('Link release connecting to node with no slots', async ({ - comfyPage - }) => { - await comfyPage.disconnectEdge() - await expect(comfyPage.searchBox.input).toHaveCount(1) - await comfyPage.page.locator('.p-chip-remove-icon').click() - await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') - await expect(comfyPage.canvas).toHaveScreenshot( - 'added-node-no-connection.png' - ) - }) + ) test('Has correct aria-labels on search results', async ({ comfyPage }) => { const node = 'Load Checkpoint' @@ -251,40 +257,45 @@ test.describe('Node search box', () => { }) }) -test.describe('Release context menu', () => { +test.describe('Release context menu', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') }) - test('Can trigger on link release', async ({ comfyPage }) => { - await comfyPage.disconnectEdge() - const contextMenu = comfyPage.page.locator('.litecontextmenu') - // Wait for context menu with correct title (slot name | slot type) - // The title shows the output slot name and type from the disconnected link - await expect(contextMenu.locator('.litemenu-title')).toContainText( - 'CLIP | CLIP' - ) - await comfyPage.page.mouse.move(10, 10) - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot( - 'link-release-context-menu.png' - ) - }) + test( + 'Can trigger on link release', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.disconnectEdge() + const contextMenu = comfyPage.page.locator('.litecontextmenu') + // Wait for context menu with correct title (slot name | slot type) + // The title shows the output slot name and type from the disconnected link + await expect(contextMenu.locator('.litemenu-title')).toContainText( + 'CLIP | CLIP' + ) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'link-release-context-menu.png' + ) + } + ) - test('Can search and add node from context menu', async ({ - comfyPage, - comfyMouse - }) => { - await comfyPage.disconnectEdge() - await comfyMouse.move({ x: 10, y: 10 }) - await comfyPage.clickContextMenuItem('Search') - await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt') - await expect(comfyPage.canvas).toHaveScreenshot( - 'link-context-menu-search.png' - ) - }) + test( + 'Can search and add node from context menu', + { tag: '@screenshot' }, + async ({ comfyPage, comfyMouse }) => { + await comfyPage.disconnectEdge() + await comfyMouse.move({ x: 10, y: 10 }) + await comfyPage.clickContextMenuItem('Search') + await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt') + await expect(comfyPage.canvas).toHaveScreenshot( + 'link-context-menu-search.png' + ) + } + ) test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({ comfyPage diff --git a/browser_tests/tests/noteNode.spec.ts b/browser_tests/tests/noteNode.spec.ts index 52dc57542..4bb551962 100644 --- a/browser_tests/tests/noteNode.spec.ts +++ b/browser_tests/tests/noteNode.spec.ts @@ -6,8 +6,8 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Note Node', () => { - test('Can load node nodes', async ({ comfyPage }) => { +test.describe('Note Node', { tag: '@node' }, () => { + test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.loadWorkflow('nodes/note_nodes') await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png') }) diff --git a/browser_tests/tests/primitiveNode.spec.ts b/browser_tests/tests/primitiveNode.spec.ts index 0584a3bec..2e2e11ae5 100644 --- a/browser_tests/tests/primitiveNode.spec.ts +++ b/browser_tests/tests/primitiveNode.spec.ts @@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Primitive Node', () => { +test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { test('Can load with correct size', async ({ comfyPage }) => { await comfyPage.loadWorkflow('primitive/primitive_node') await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png') diff --git a/browser_tests/tests/recordAudio.spec.ts b/browser_tests/tests/recordAudio.spec.ts index cc0ac3684..d343990c1 100644 --- a/browser_tests/tests/recordAudio.spec.ts +++ b/browser_tests/tests/recordAudio.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Record Audio Node', () => { +test.describe('Record Audio Node', { tag: '@screenshot' }, () => { test('should add a record audio node and take a screenshot', async ({ comfyPage }) => { diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index 18cc77f44..cc7cad64f 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import type { ComfyPage } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Remote COMBO Widget', () => { +test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { const mockOptions = ['d', 'c', 'b', 'a'] const addRemoteWidgetNode = async ( diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 0b2b1e0f6..48f155419 100644 --- a/browser_tests/tests/rerouteNode.spec.ts +++ b/browser_tests/tests/rerouteNode.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { getMiddlePoint } from '../fixtures/utils/litegraphUtils' -test.describe('Reroute Node', () => { +test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') }) @@ -38,92 +38,96 @@ test.describe('Reroute Node', () => { }) }) -test.describe('LiteGraph Native Reroute Node', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') - await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) - }) +test.describe( + 'LiteGraph Native Reroute Node', + { tag: ['@screenshot', '@node'] }, + () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) + }) - test('loads from workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('reroute/native_reroute') - await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png') - }) + test('loads from workflow', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('reroute/native_reroute') + await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png') + }) - test('@2x @0.5x Can add reroute by alt clicking on link', async ({ - comfyPage - }) => { - const loadCheckpointNode = ( - await comfyPage.getNodeRefsByTitle('Load Checkpoint') - )[0] - const clipEncodeNode = ( - await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') - )[0] + test('@2x @0.5x Can add reroute by alt clicking on link', async ({ + comfyPage + }) => { + const loadCheckpointNode = ( + await comfyPage.getNodeRefsByTitle('Load Checkpoint') + )[0] + const clipEncodeNode = ( + await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') + )[0] - const slot1 = await loadCheckpointNode.getOutput(1) - const slot2 = await clipEncodeNode.getInput(0) - const middlePoint = getMiddlePoint( - await slot1.getPosition(), - await slot2.getPosition() - ) + const slot1 = await loadCheckpointNode.getOutput(1) + const slot2 = await clipEncodeNode.getInput(0) + const middlePoint = getMiddlePoint( + await slot1.getPosition(), + await slot2.getPosition() + ) - await comfyPage.page.keyboard.down('Alt') - await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) - await comfyPage.page.keyboard.up('Alt') + await comfyPage.page.keyboard.down('Alt') + await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) + await comfyPage.page.keyboard.up('Alt') - await expect(comfyPage.canvas).toHaveScreenshot( - 'native_reroute_alt_click.png' - ) - }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'native_reroute_alt_click.png' + ) + }) - test('Can add reroute by clicking middle of link context menu', async ({ - comfyPage - }) => { - const loadCheckpointNode = ( - await comfyPage.getNodeRefsByTitle('Load Checkpoint') - )[0] - const clipEncodeNode = ( - await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') - )[0] + test('Can add reroute by clicking middle of link context menu', async ({ + comfyPage + }) => { + const loadCheckpointNode = ( + await comfyPage.getNodeRefsByTitle('Load Checkpoint') + )[0] + const clipEncodeNode = ( + await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') + )[0] - const slot1 = await loadCheckpointNode.getOutput(1) - const slot2 = await clipEncodeNode.getInput(0) - const middlePoint = getMiddlePoint( - await slot1.getPosition(), - await slot2.getPosition() - ) + const slot1 = await loadCheckpointNode.getOutput(1) + const slot2 = await clipEncodeNode.getInput(0) + const middlePoint = getMiddlePoint( + await slot1.getPosition(), + await slot2.getPosition() + ) - await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) - await comfyPage.page - .locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' }) - .click() + await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) + await comfyPage.page + .locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' }) + .click() - await expect(comfyPage.canvas).toHaveScreenshot( - 'native_reroute_context_menu.png' - ) - }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'native_reroute_context_menu.png' + ) + }) - test('Can delete link that is connected to two reroutes', async ({ - comfyPage - }) => { - // https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695 - await comfyPage.loadWorkflow( - 'reroute/single-native-reroute-default-workflow' - ) + test('Can delete link that is connected to two reroutes', async ({ + comfyPage + }) => { + // https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695 + await comfyPage.loadWorkflow( + 'reroute/single-native-reroute-default-workflow' + ) - // To find the clickable midpoint button, we use the hardcoded value from the browser logs - // since the link is a bezier curve and not a straight line. - const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 } + // To find the clickable midpoint button, we use the hardcoded value from the browser logs + // since the link is a bezier curve and not a straight line. + const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 } - // Click the middle point of the link to open the context menu. - await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) + // Click the middle point of the link to open the context menu. + await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y) - // Click the "Delete" context menu option. - await comfyPage.page - .locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' }) - .click() + // Click the "Delete" context menu option. + await comfyPage.page + .locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' }) + .click() - await expect(comfyPage.canvas).toHaveScreenshot( - 'native_reroute_delete_from_midpoint_context_menu.png' - ) - }) -}) + await expect(comfyPage.canvas).toHaveScreenshot( + 'native_reroute_delete_from_midpoint_context_menu.png' + ) + }) + } +) diff --git a/browser_tests/tests/rightClickMenu.spec.ts b/browser_tests/tests/rightClickMenu.spec.ts index d2c17b43d..3114a9456 100644 --- a/browser_tests/tests/rightClickMenu.spec.ts +++ b/browser_tests/tests/rightClickMenu.spec.ts @@ -7,43 +7,49 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Canvas Right Click Menu', () => { - test('Can add node', async ({ comfyPage }) => { - await comfyPage.rightClickCanvas() - await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') - await comfyPage.page.getByText('Add Node').click() - await comfyPage.nextFrame() - await comfyPage.page.getByText('loaders').click() - await comfyPage.nextFrame() - await comfyPage.page.getByText('Load VAE').click() - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png') - }) +test.describe( + 'Canvas Right Click Menu', + { tag: ['@screenshot', '@ui'] }, + () => { + test('Can add node', async ({ comfyPage }) => { + await comfyPage.rightClickCanvas() + await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') + await comfyPage.page.getByText('Add Node').click() + await comfyPage.nextFrame() + await comfyPage.page.getByText('loaders').click() + await comfyPage.nextFrame() + await comfyPage.page.getByText('Load VAE').click() + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png') + }) - test('Can add group', async ({ comfyPage }) => { - await comfyPage.rightClickCanvas() - await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') - await comfyPage.page.getByText('Add Group', { exact: true }).click() - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png') - }) + test('Can add group', async ({ comfyPage }) => { + await comfyPage.rightClickCanvas() + await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') + await comfyPage.page.getByText('Add Group', { exact: true }).click() + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'add-group-group-added.png' + ) + }) - test('Can convert to group node', async ({ comfyPage }) => { - await comfyPage.select2Nodes() - await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png') - await comfyPage.rightClickCanvas() - await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)') - await comfyPage.promptDialogInput.fill('GroupNode2CLIP') - await comfyPage.page.keyboard.press('Enter') - await comfyPage.promptDialogInput.waitFor({ state: 'hidden' }) - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot( - 'right-click-node-group-node.png' - ) - }) -}) + test('Can convert to group node', async ({ comfyPage }) => { + await comfyPage.select2Nodes() + await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png') + await comfyPage.rightClickCanvas() + await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)') + await comfyPage.promptDialogInput.fill('GroupNode2CLIP') + await comfyPage.page.keyboard.press('Enter') + await comfyPage.promptDialogInput.waitFor({ state: 'hidden' }) + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'right-click-node-group-node.png' + ) + }) + } +) -test.describe('Node Right Click Menu', () => { +test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { test('Can open properties panel', async ({ comfyPage }) => { await comfyPage.rightClickEmptyLatentNode() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') diff --git a/browser_tests/tests/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index 6b8576982..fd74a4aa7 100644 --- a/browser_tests/tests/selectionToolbox.spec.ts +++ b/browser_tests/tests/selectionToolbox.spec.ts @@ -10,7 +10,7 @@ test.beforeEach(async ({ comfyPage }) => { const BLUE_COLOR = 'rgb(51, 51, 85)' const RED_COLOR = 'rgb(85, 51, 51)' -test.describe('Selection Toolbox', () => { +test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) }) diff --git a/browser_tests/tests/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts index fd4c55faf..c5d74c167 100644 --- a/browser_tests/tests/selectionToolboxSubmenus.spec.ts +++ b/browser_tests/tests/selectionToolboxSubmenus.spec.ts @@ -7,178 +7,190 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Selection Toolbox - More Options Submenus', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.loadWorkflow('nodes/single_ksampler') - await comfyPage.nextFrame() - await comfyPage.selectNodes(['KSampler']) - await comfyPage.nextFrame() - }) - - const openMoreOptions = async (comfyPage: ComfyPage) => { - const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') - if (ksamplerNodes.length === 0) { - throw new Error('No KSampler nodes found') - } - - // Drag the KSampler to the center of the screen - const nodePos = await ksamplerNodes[0].getPosition() - const viewportSize = comfyPage.page.viewportSize() - const centerX = viewportSize.width / 3 - const centerY = viewportSize.height / 2 - await comfyPage.dragAndDrop( - { x: nodePos.x, y: nodePos.y }, - { x: centerX, y: centerY } - ) - await comfyPage.nextFrame() - - await ksamplerNodes[0].click('title') - await comfyPage.nextFrame() - - await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({ - timeout: 5000 - }) - - const moreOptionsBtn = comfyPage.page.locator( - '[data-testid="more-options-button"]' - ) - await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 }) - - await comfyPage.page.click('[data-testid="more-options-button"]') - - await comfyPage.nextFrame() - - const menuOptionsVisible = await comfyPage.page - .getByText('Rename') - .isVisible({ timeout: 2000 }) - .catch(() => false) - if (menuOptionsVisible) { - return - } - - await moreOptionsBtn.click({ force: true }) - await comfyPage.nextFrame() - - const menuOptionsVisibleAfterClick = await comfyPage.page - .getByText('Rename') - .isVisible({ timeout: 2000 }) - .catch(() => false) - if (menuOptionsVisibleAfterClick) { - return - } - - throw new Error('Could not open More Options menu - popover not showing') - } - - test('opens Node Info from More Options menu', async ({ comfyPage }) => { - await openMoreOptions(comfyPage) - const nodeInfoButton = comfyPage.page.getByText('Node Info', { - exact: true - }) - await expect(nodeInfoButton).toBeVisible() - await nodeInfoButton.click() - await comfyPage.nextFrame() - }) - - test('changes node shape via Shape submenu', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] - const initialShape = await nodeRef.getProperty('shape') - - await openMoreOptions(comfyPage) - await comfyPage.page.getByText('Shape', { exact: true }).hover() - await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({ - timeout: 5000 - }) - await comfyPage.page.getByText('Box', { exact: true }).click() - await comfyPage.nextFrame() - - const newShape = await nodeRef.getProperty('shape') - expect(newShape).not.toBe(initialShape) - expect(newShape).toBe(1) - }) - - test('changes node color via Color submenu swatch', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] - const initialColor = await nodeRef.getProperty('color') - - await openMoreOptions(comfyPage) - await comfyPage.page.getByText('Color', { exact: true }).click() - const blueSwatch = comfyPage.page.locator('[title="Blue"]') - await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 }) - await blueSwatch.first().click() - await comfyPage.nextFrame() - - const newColor = await nodeRef.getProperty('color') - expect(newColor).toBe('#223') - if (initialColor) { - expect(newColor).not.toBe(initialColor) - } - }) - - test('renames a node using Rename action', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] - await openMoreOptions(comfyPage) - await comfyPage.page - .getByText('Rename', { exact: true }) - .click({ force: true }) - const input = comfyPage.page.locator( - '.group-title-editor.node-title-editor .editable-text input' - ) - await expect(input).toBeVisible() - await input.fill('RenamedNode') - await input.press('Enter') - await comfyPage.nextFrame() - const newTitle = await nodeRef.getProperty('title') - expect(newTitle).toBe('RenamedNode') - }) - - test('closes More Options menu when clicking outside', async ({ - comfyPage - }) => { - await openMoreOptions(comfyPage) - const renameItem = comfyPage.page.getByText('Rename', { exact: true }) - await expect(renameItem).toBeVisible({ timeout: 5000 }) - - // Wait for multiple frames to allow PrimeVue's outside click handler to initialize - for (let i = 0; i < 30; i++) { +test.describe( + 'Selection Toolbox - More Options Submenus', + { tag: '@ui' }, + () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.nextFrame() + await comfyPage.selectNodes(['KSampler']) + await comfyPage.nextFrame() + }) + + const openMoreOptions = async (comfyPage: ComfyPage) => { + const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') + if (ksamplerNodes.length === 0) { + throw new Error('No KSampler nodes found') + } + + // Drag the KSampler to the center of the screen + const nodePos = await ksamplerNodes[0].getPosition() + const viewportSize = comfyPage.page.viewportSize() + const centerX = viewportSize.width / 3 + const centerY = viewportSize.height / 2 + await comfyPage.dragAndDrop( + { x: nodePos.x, y: nodePos.y }, + { x: centerX, y: centerY } + ) + await comfyPage.nextFrame() + + await ksamplerNodes[0].click('title') + await comfyPage.nextFrame() + + await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({ + timeout: 5000 + }) + + const moreOptionsBtn = comfyPage.page.locator( + '[data-testid="more-options-button"]' + ) + await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 }) + + await comfyPage.page.click('[data-testid="more-options-button"]') + + await comfyPage.nextFrame() + + const menuOptionsVisible = await comfyPage.page + .getByText('Rename') + .isVisible({ timeout: 2000 }) + .catch(() => false) + if (menuOptionsVisible) { + return + } + + await moreOptionsBtn.click({ force: true }) + await comfyPage.nextFrame() + + const menuOptionsVisibleAfterClick = await comfyPage.page + .getByText('Rename') + .isVisible({ timeout: 2000 }) + .catch(() => false) + if (menuOptionsVisibleAfterClick) { + return + } + + throw new Error('Could not open More Options menu - popover not showing') } - await comfyPage.page - .locator('#graph-canvas') - .click({ position: { x: 0, y: 50 }, force: true }) + test('opens Node Info from More Options menu', async ({ comfyPage }) => { + await openMoreOptions(comfyPage) + const nodeInfoButton = comfyPage.page.getByText('Node Info', { + exact: true + }) + await expect(nodeInfoButton).toBeVisible() + await nodeInfoButton.click() + await comfyPage.nextFrame() + }) - await comfyPage.nextFrame() - await expect( - comfyPage.page.getByText('Rename', { exact: true }) - ).not.toBeVisible() - }) + test('changes node shape via Shape submenu', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const initialShape = await nodeRef.getProperty('shape') - test('closes More Options menu when clicking the button again (toggle)', async ({ - comfyPage - }) => { - await openMoreOptions(comfyPage) - await expect( - comfyPage.page.getByText('Rename', { exact: true }) - ).toBeVisible({ timeout: 5000 }) + await openMoreOptions(comfyPage) + await comfyPage.page.getByText('Shape', { exact: true }).hover() + await expect( + comfyPage.page.getByText('Box', { exact: true }) + ).toBeVisible({ + timeout: 5000 + }) + await comfyPage.page.getByText('Box', { exact: true }).click() + await comfyPage.nextFrame() - await comfyPage.page.evaluate(() => { - const btn = document.querySelector('[data-testid="more-options-button"]') - if (btn) { - const event = new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window, - detail: 1 - }) - btn.dispatchEvent(event) + const newShape = await nodeRef.getProperty('shape') + expect(newShape).not.toBe(initialShape) + expect(newShape).toBe(1) + }) + + test('changes node color via Color submenu swatch', async ({ + comfyPage + }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const initialColor = await nodeRef.getProperty( + 'color' + ) + + await openMoreOptions(comfyPage) + await comfyPage.page.getByText('Color', { exact: true }).click() + const blueSwatch = comfyPage.page.locator('[title="Blue"]') + await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 }) + await blueSwatch.first().click() + await comfyPage.nextFrame() + + const newColor = await nodeRef.getProperty('color') + expect(newColor).toBe('#223') + if (initialColor) { + expect(newColor).not.toBe(initialColor) } }) - await comfyPage.nextFrame() - await expect( - comfyPage.page.getByText('Rename', { exact: true }) - ).not.toBeVisible() - }) -}) + test('renames a node using Rename action', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + await openMoreOptions(comfyPage) + await comfyPage.page + .getByText('Rename', { exact: true }) + .click({ force: true }) + const input = comfyPage.page.locator( + '.group-title-editor.node-title-editor .editable-text input' + ) + await expect(input).toBeVisible() + await input.fill('RenamedNode') + await input.press('Enter') + await comfyPage.nextFrame() + const newTitle = await nodeRef.getProperty('title') + expect(newTitle).toBe('RenamedNode') + }) + + test('closes More Options menu when clicking outside', async ({ + comfyPage + }) => { + await openMoreOptions(comfyPage) + const renameItem = comfyPage.page.getByText('Rename', { exact: true }) + await expect(renameItem).toBeVisible({ timeout: 5000 }) + + // Wait for multiple frames to allow PrimeVue's outside click handler to initialize + for (let i = 0; i < 30; i++) { + await comfyPage.nextFrame() + } + + await comfyPage.page + .locator('#graph-canvas') + .click({ position: { x: 0, y: 50 }, force: true }) + + await comfyPage.nextFrame() + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).not.toBeVisible() + }) + + test('closes More Options menu when clicking the button again (toggle)', async ({ + comfyPage + }) => { + await openMoreOptions(comfyPage) + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).toBeVisible({ timeout: 5000 }) + + await comfyPage.page.evaluate(() => { + const btn = document.querySelector( + '[data-testid="more-options-button"]' + ) + if (btn) { + const event = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + detail: 1 + }) + btn.dispatchEvent(event) + } + }) + await comfyPage.nextFrame() + + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).not.toBeVisible() + }) + } +) diff --git a/browser_tests/tests/subgraph-rename-dialog.spec.ts b/browser_tests/tests/subgraph-rename-dialog.spec.ts index 0bdb9766d..f917bdb9b 100644 --- a/browser_tests/tests/subgraph-rename-dialog.spec.ts +++ b/browser_tests/tests/subgraph-rename-dialog.spec.ts @@ -12,7 +12,7 @@ const SELECTORS = { promptDialog: '.graphdialog input' } as const -test.describe('Subgraph Slot Rename Dialog', () => { +test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) diff --git a/browser_tests/tests/subgraph.spec.ts b/browser_tests/tests/subgraph.spec.ts index 635915621..1719ba059 100644 --- a/browser_tests/tests/subgraph.spec.ts +++ b/browser_tests/tests/subgraph.spec.ts @@ -16,7 +16,7 @@ const SELECTORS = { domWidget: '.comfy-multiline-input' } as const -test.describe('Subgraph Operations', () => { +test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 4921165f7..334c761af 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -13,7 +13,7 @@ async function checkTemplateFileExists( return response.ok() } -test.describe('Templates', () => { +test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false) @@ -207,109 +207,114 @@ test.describe('Templates', () => { await expect(nav).toBeVisible() // Nav should be visible at tablet size }) - test('template cards descriptions adjust height dynamically', async ({ - comfyPage - }) => { - // Setup test by intercepting templates response to inject cards with varying description lengths - await comfyPage.page.route('**/templates/index.json', async (route, _) => { - const response = [ - { - moduleName: 'default', - title: 'Test Templates', - type: 'image', - templates: [ + test( + 'template cards descriptions adjust height dynamically', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + // Setup test by intercepting templates response to inject cards with varying description lengths + await comfyPage.page.route( + '**/templates/index.json', + async (route, _) => { + const response = [ { - name: 'short-description', - title: 'Short Description', - mediaType: 'image', - mediaSubtype: 'webp', - description: 'This is a short description.' - }, - { - name: 'medium-description', - title: 'Medium Description', - mediaType: 'image', - mediaSubtype: 'webp', - description: - 'This is a medium length description that should take up two lines on most displays.' - }, - { - name: 'long-description', - title: 'Long Description', - mediaType: 'image', - mediaSubtype: 'webp', - description: - 'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.' + moduleName: 'default', + title: 'Test Templates', + type: 'image', + templates: [ + { + name: 'short-description', + title: 'Short Description', + mediaType: 'image', + mediaSubtype: 'webp', + description: 'This is a short description.' + }, + { + name: 'medium-description', + title: 'Medium Description', + mediaType: 'image', + mediaSubtype: 'webp', + description: + 'This is a medium length description that should take up two lines on most displays.' + }, + { + name: 'long-description', + title: 'Long Description', + mediaType: 'image', + mediaSubtype: 'webp', + description: + 'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.' + } + ] } ] + await route.fulfill({ + status: 200, + body: JSON.stringify(response), + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store' + } + }) } - ] - await route.fulfill({ - status: 200, - body: JSON.stringify(response), - headers: { - 'Content-Type': 'application/json', + ) + + // Mock the thumbnail images to avoid 404s + await comfyPage.page.route('**/templates/**.webp', async (route) => { + const headers = { + 'Content-Type': 'image/webp', 'Cache-Control': 'no-store' } + await route.fulfill({ + status: 200, + path: 'browser_tests/assets/example.webp', + headers + }) }) - }) - // Mock the thumbnail images to avoid 404s - await comfyPage.page.route('**/templates/**.webp', async (route) => { - const headers = { - 'Content-Type': 'image/webp', - 'Cache-Control': 'no-store' - } - await route.fulfill({ - status: 200, - path: 'browser_tests/assets/example.webp', - headers - }) - }) + // Open templates dialog + await comfyPage.executeCommand('Comfy.BrowseTemplates') + await expect(comfyPage.templates.content).toBeVisible() - // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') - await expect(comfyPage.templates.content).toBeVisible() + // Wait for cards to load + await expect( + comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) + ).toBeVisible({ timeout: 5000 }) - // Wait for cards to load - await expect( - comfyPage.page.locator( + // Verify all three cards with different descriptions are visible + const shortDescCard = comfyPage.page.locator( '[data-testid="template-workflow-short-description"]' ) - ).toBeVisible({ timeout: 5000 }) + const mediumDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-medium-description"]' + ) + const longDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-long-description"]' + ) - // Verify all three cards with different descriptions are visible - const shortDescCard = comfyPage.page.locator( - '[data-testid="template-workflow-short-description"]' - ) - const mediumDescCard = comfyPage.page.locator( - '[data-testid="template-workflow-medium-description"]' - ) - const longDescCard = comfyPage.page.locator( - '[data-testid="template-workflow-long-description"]' - ) + await expect(shortDescCard).toBeVisible() + await expect(mediumDescCard).toBeVisible() + await expect(longDescCard).toBeVisible() - await expect(shortDescCard).toBeVisible() - await expect(mediumDescCard).toBeVisible() - await expect(longDescCard).toBeVisible() + // Verify descriptions are visible and have line-clamp class + // The description is in a p tag with text-muted class + const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2') + const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2') + const longDesc = longDescCard.locator('p.text-muted.line-clamp-2') - // Verify descriptions are visible and have line-clamp class - // The description is in a p tag with text-muted class - const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2') - const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2') - const longDesc = longDescCard.locator('p.text-muted.line-clamp-2') + await expect(shortDesc).toContainText('short description') + await expect(mediumDesc).toContainText('medium length description') + await expect(longDesc).toContainText('much longer description') - await expect(shortDesc).toContainText('short description') - await expect(mediumDesc).toContainText('medium length description') - await expect(longDesc).toContainText('much longer description') - - // Verify grid layout maintains consistency - const templateGrid = comfyPage.page.locator( - '[data-testid="template-workflows-content"]' - ) - await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot( - 'template-grid-varying-content.png' - ) - }) + // Verify grid layout maintains consistency + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) + await expect(templateGrid).toBeVisible() + await expect(templateGrid).toHaveScreenshot( + 'template-grid-varying-content.png' + ) + } + ) }) diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index f022bb4bd..aa3481408 100644 --- a/browser_tests/tests/useSettingSearch.spec.ts +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Settings Search functionality', () => { +test.describe('Settings Search functionality', { tag: '@settings' }, () => { test.beforeEach(async ({ comfyPage }) => { // Register test settings to verify hidden/deprecated filtering await comfyPage.page.evaluate(() => { diff --git a/browser_tests/tests/userSelectView.spec.ts b/browser_tests/tests/userSelectView.spec.ts index 38b1a2317..45eccb0c1 100644 --- a/browser_tests/tests/userSelectView.spec.ts +++ b/browser_tests/tests/userSelectView.spec.ts @@ -5,7 +5,7 @@ import { userSelectPageFixture as test } from '../fixtures/UserSelectPage' /** * Expects ComfyUI backend to be launched with `--multi-user` flag. */ -test.describe('User Select View', () => { +test.describe('User Select View', { tag: '@settings' }, () => { test.beforeEach(async ({ userSelectPage, page }) => { await page.goto(userSelectPage.url) await page.evaluate(() => { diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts index 223770b12..15c55873f 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import type { SystemStats } from '../../src/schemas/apiSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Version Mismatch Warnings', () => { +test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => { const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100' const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0' diff --git a/browser_tests/tests/viewport.spec.ts b/browser_tests/tests/viewport.spec.ts index 7bc686a1c..c0d65ca38 100644 --- a/browser_tests/tests/viewport.spec.ts +++ b/browser_tests/tests/viewport.spec.ts @@ -2,7 +2,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' -test.describe('Viewport', () => { +test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => { test('Fits view to nodes when saved viewport position is offscreen', async ({ comfyPage }) => { diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts b/browser_tests/tests/vueNodes/groups/groups.spec.ts index c12c1772b..19404f7d4 100644 --- a/browser_tests/tests/vueNodes/groups/groups.spec.ts +++ b/browser_tests/tests/vueNodes/groups/groups.spec.ts @@ -5,7 +5,7 @@ import { const CREATE_GROUP_HOTKEY = 'Control+g' -test.describe('Vue Node Groups', () => { +test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true) diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts index a91a9b928..ed18ab367 100644 --- a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts @@ -9,10 +9,14 @@ test.describe('Vue Nodes Canvas Pan', () => { await comfyPage.vueNodes.waitForNodes() }) - test('@mobile Can pan with touch', async ({ comfyPage }) => { - await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 }) - await expect(comfyPage.canvas).toHaveScreenshot( - 'vue-nodes-paned-with-touch.png' - ) - }) + test( + '@mobile Can pan with touch', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-nodes-paned-with-touch.png' + ) + } + ) }) diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index f1d41cf13..915fb36d9 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 b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts index 216623337..0b064dd81 100644 --- a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts @@ -10,25 +10,30 @@ test.describe('Vue Nodes Zoom', () => { await comfyPage.vueNodes.waitForNodes() }) - test('should not capture drag while zooming with ctrl+shift+drag', async ({ - comfyPage - }) => { - const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') - const nodeBoundingBox = await checkpointNode.boundingBox() - if (!nodeBoundingBox) throw new Error('Node bounding box not available') + test( + 'should not capture drag while zooming with ctrl+shift+drag', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + const checkpointNode = + comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const nodeBoundingBox = await checkpointNode.boundingBox() + if (!nodeBoundingBox) throw new Error('Node bounding box not available') - const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2 - const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2 + const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2 + const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2 - // Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over - // the node. The node should not capture the drag while drag-zooming. - await comfyPage.page.keyboard.down('Control') - await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop( - { x: 200, y: 300 }, - { x: nodeMidpointX, y: nodeMidpointY } - ) + // Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over + // the node. The node should not capture the drag while drag-zooming. + await comfyPage.page.keyboard.down('Control') + await comfyPage.page.keyboard.down('Shift') + await comfyPage.dragAndDrop( + { x: 200, y: 300 }, + { x: nodeMidpointX, y: nodeMidpointY } + ) - await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png') - }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'zoomed-in-ctrl-shift.png' + ) + } + ) }) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index 9b007afb2..b22baf5b1 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -98,7 +98,7 @@ async function connectSlots( await nextFrame() } -test.describe('Vue Node Link Interaction', () => { +test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts index 6e20f9e36..07b23e088 100644 --- a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts @@ -5,7 +5,7 @@ import { import type { ComfyPage } from '../../../../fixtures/ComfyPage' import { fitToViewInstant } from '../../../../helpers/fitToView' -test.describe('Vue Node Bring to Front', () => { +test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts index ac4759934..fd74872e5 100644 --- a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts @@ -1,8 +1,8 @@ import { - type ComfyPage, comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' +import type { ComfyPage } from '../../../../fixtures/ComfyPage' import type { Position } from '../../../../fixtures/types' test.describe('Vue Node Moving', () => { @@ -29,39 +29,47 @@ test.describe('Vue Node Moving', () => { expect(diffY).toBeGreaterThan(0) } - test('should allow moving nodes by dragging', async ({ comfyPage }) => { - const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await comfyPage.dragAndDrop(loadCheckpointHeaderPos, { - x: 256, - y: 256 - }) + test( + 'should allow moving nodes by dragging', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + const loadCheckpointHeaderPos = + await getLoadCheckpointHeaderPos(comfyPage) + await comfyPage.dragAndDrop(loadCheckpointHeaderPos, { + x: 256, + y: 256 + }) - const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos) + const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) + await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos) - await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png') - }) + await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png') + } + ) - test('@mobile should allow moving nodes by dragging on touch devices', async ({ - comfyPage - }) => { - // Disable minimap (gets in way of the node on small screens) - await comfyPage.setSetting('Comfy.Minimap.Visible', false) + test( + '@mobile should allow moving nodes by dragging on touch devices', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + // Disable minimap (gets in way of the node on small screens) + await comfyPage.setSetting('Comfy.Minimap.Visible', false) - const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await comfyPage.panWithTouch( - { - x: 64, - y: 64 - }, - loadCheckpointHeaderPos - ) + const loadCheckpointHeaderPos = + await getLoadCheckpointHeaderPos(comfyPage) + await comfyPage.panWithTouch( + { + x: 64, + y: 64 + }, + loadCheckpointHeaderPos + ) - const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos) + const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) + await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos) - await expect(comfyPage.canvas).toHaveScreenshot( - 'vue-node-moved-node-touch.png' - ) - }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-moved-node-touch.png' + ) + } + ) }) 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 390def0a2..f4bac78b7 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/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts index 7074bbc2d..fb9ff2872 100644 --- a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -15,22 +15,25 @@ test.describe('Vue Node Bypass', () => { await comfyPage.vueNodes.waitForNodes() }) - test('should allow toggling bypass on a selected node with hotkey', async ({ - comfyPage - }) => { - await comfyPage.page.getByText('Load Checkpoint').click() - await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + test( + 'should allow toggling bypass on a selected node with hotkey', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) - const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') - await expect(checkpointNode).toHaveClass(BYPASS_CLASS) - await comfyPage.nextFrame() - await expect(comfyPage.canvas).toHaveScreenshot( - 'vue-node-bypassed-state.png' - ) + const checkpointNode = + comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-bypassed-state.png' + ) - await comfyPage.page.keyboard.press(BYPASS_HOTKEY) - await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) - }) + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + } + ) test('should allow toggling bypass on multiple selected nodes with hotkey', async ({ comfyPage diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts index e61e3ca01..369c7e195 100644 --- a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts @@ -3,7 +3,7 @@ import { comfyPageFixture as test } from '../../../fixtures/ComfyPage' -test.describe('Vue Node Custom Colors', () => { +test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts index 3fe656ebc..e59d79cfd 100644 --- a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -12,19 +12,24 @@ test.describe('Vue Node Mute', () => { await comfyPage.vueNodes.waitForNodes() }) - test('should allow toggling mute on a selected node with hotkey', async ({ - comfyPage - }) => { - await comfyPage.page.getByText('Load Checkpoint').click() - await comfyPage.page.keyboard.press(MUTE_HOTKEY) + test( + 'should allow toggling mute on a selected node with hotkey', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(MUTE_HOTKEY) - const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') - await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY) - await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png') + const checkpointNode = + comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + 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.toHaveCSS('opacity', MUTE_OPACITY) - }) + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY) + } + ) test('should allow toggling mute on multiple selected nodes with hotkey', async ({ comfyPage diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts index 8fcc3360d..2ae178d22 100644 --- a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts @@ -9,13 +9,17 @@ test.describe('Vue Upload Widgets', () => { await comfyPage.vueNodes.waitForNodes() }) - test('should hide canvas-only upload buttons', async ({ comfyPage }) => { - await comfyPage.setup() - await comfyPage.loadWorkflow('widgets/all_load_widgets') - await comfyPage.vueNodes.waitForNodes() + test( + 'should hide canvas-only upload buttons', + { tag: '@screenshot' }, + async ({ comfyPage }) => { + await comfyPage.setup() + await comfyPage.loadWorkflow('widgets/all_load_widgets') + await comfyPage.vueNodes.waitForNodes() - await expect(comfyPage.canvas).toHaveScreenshot( - 'vue-nodes-upload-widgets.png' - ) - }) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-nodes-upload-widgets.png' + ) + } + ) }) 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 3d0ae2ed0..745415a11 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/widget.spec.ts b/browser_tests/tests/widget.spec.ts index 075854860..a0dfb0a6e 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') }) -test.describe('Combo text widget', () => { +test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { test('Truncates text when resized', async ({ comfyPage }) => { await comfyPage.resizeLoadCheckpointNode(0.2, 1) await expect(comfyPage.canvas).toHaveScreenshot( @@ -79,7 +79,7 @@ test.describe('Combo text widget', () => { }) }) -test.describe('Boolean widget', () => { +test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can toggle', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/boolean_widget') await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png') @@ -92,7 +92,7 @@ test.describe('Boolean widget', () => { }) }) -test.describe('Slider widget', () => { +test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can drag adjust value', async ({ comfyPage }) => { await comfyPage.loadWorkflow('inputs/simple_slider') const node = (await comfyPage.getFirstNodeRef())! @@ -113,7 +113,7 @@ test.describe('Slider widget', () => { }) }) -test.describe('Number widget', () => { +test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can drag adjust value', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/seed_widget') @@ -134,22 +134,28 @@ test.describe('Number widget', () => { }) }) -test.describe('Dynamic widget manipulation', () => { - test('Auto expand node when widget is added dynamically', async ({ - comfyPage - }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') +test.describe( + 'Dynamic widget manipulation', + { tag: ['@screenshot', '@widget'] }, + () => { + test('Auto expand node when widget is added dynamically', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('nodes/single_ksampler') - await comfyPage.page.evaluate(() => { - window['graph'].nodes[0].addWidget('number', 'new_widget', 10) - window['graph'].setDirtyCanvas(true, true) + await comfyPage.page.evaluate(() => { + window['graph'].nodes[0].addWidget('number', 'new_widget', 10) + window['graph'].setDirtyCanvas(true, true) + }) + + await expect(comfyPage.canvas).toHaveScreenshot( + 'ksampler_widget_added.png' + ) }) + } +) - await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png') - }) -}) - -test.describe('Image widget', () => { +test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can load image', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/load_image_widget') await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png') @@ -236,99 +242,103 @@ test.describe('Image widget', () => { }) }) -test.describe('Animated image widget', () => { - // https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718 - test.skip('Shows preview of uploaded animated image', async ({ - comfyPage - }) => { - await comfyPage.loadWorkflow('widgets/load_animated_webp') +test.describe( + 'Animated image widget', + { tag: ['@screenshot', '@widget'] }, + () => { + // https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718 + test.skip('Shows preview of uploaded animated image', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('widgets/load_animated_webp') - // Get position of the load animated webp node - const nodes = await comfyPage.getNodeRefsByType( - 'DevToolsLoadAnimatedImageTest' - ) - const loadAnimatedWebpNode = nodes[0] - const { x, y } = await loadAnimatedWebpNode.getPosition() + // Get position of the load animated webp node + const nodes = await comfyPage.getNodeRefsByType( + 'DevToolsLoadAnimatedImageTest' + ) + const loadAnimatedWebpNode = nodes[0] + const { x, y } = await loadAnimatedWebpNode.getPosition() - // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { - dropPosition: { x, y } + // Drag and drop image file onto the load animated webp node + await comfyPage.dragAndDropFile('animated_webp.webp', { + dropPosition: { x, y } + }) + + // Expect the image preview to change automatically + await expect(comfyPage.canvas).toHaveScreenshot( + 'animated_image_preview_drag_and_dropped.png' + ) + + // Move mouse and click on canvas to trigger render + await comfyPage.page.mouse.click(64, 64) + + // Expect the image preview to change to the next frame of the animation + await expect(comfyPage.canvas).toHaveScreenshot( + 'animated_image_preview_drag_and_dropped_next_frame.png' + ) }) - // Expect the image preview to change automatically - await expect(comfyPage.canvas).toHaveScreenshot( - 'animated_image_preview_drag_and_dropped.png' - ) + test('Can drag-and-drop animated webp image', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('widgets/load_animated_webp') - // Move mouse and click on canvas to trigger render - await comfyPage.page.mouse.click(64, 64) + // Get position of the load animated webp node + const nodes = await comfyPage.getNodeRefsByType( + 'DevToolsLoadAnimatedImageTest' + ) + const loadAnimatedWebpNode = nodes[0] + const { x, y } = await loadAnimatedWebpNode.getPosition() - // Expect the image preview to change to the next frame of the animation - await expect(comfyPage.canvas).toHaveScreenshot( - 'animated_image_preview_drag_and_dropped_next_frame.png' - ) - }) + // Drag and drop image file onto the load animated webp node + await comfyPage.dragAndDropFile('animated_webp.webp', { + dropPosition: { x, y }, + waitForUpload: true + }) - test('Can drag-and-drop animated webp image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_animated_webp') - - // Get position of the load animated webp node - const nodes = await comfyPage.getNodeRefsByType( - 'DevToolsLoadAnimatedImageTest' - ) - const loadAnimatedWebpNode = nodes[0] - const { x, y } = await loadAnimatedWebpNode.getPosition() - - // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { - dropPosition: { x, y }, - waitForUpload: true + // Expect the filename combo value to be updated + const fileComboWidget = await loadAnimatedWebpNode.getWidget(0) + const filename = await fileComboWidget.getValue() + expect(filename).toContain('animated_webp.webp') }) - // Expect the filename combo value to be updated - const fileComboWidget = await loadAnimatedWebpNode.getWidget(0) - const filename = await fileComboWidget.getValue() - expect(filename).toContain('animated_webp.webp') - }) + test('Can preview saved animated webp image', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('widgets/save_animated_webp') - test('Can preview saved animated webp image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/save_animated_webp') + // Get position of the load animated webp node + const loadNodes = await comfyPage.getNodeRefsByType( + 'DevToolsLoadAnimatedImageTest' + ) + const loadAnimatedWebpNode = loadNodes[0] + const { x, y } = await loadAnimatedWebpNode.getPosition() - // Get position of the load animated webp node - const loadNodes = await comfyPage.getNodeRefsByType( - 'DevToolsLoadAnimatedImageTest' - ) - const loadAnimatedWebpNode = loadNodes[0] - const { x, y } = await loadAnimatedWebpNode.getPosition() + // Drag and drop image file onto the load animated webp node + await comfyPage.dragAndDropFile('animated_webp.webp', { + dropPosition: { x, y } + }) + await comfyPage.nextFrame() - // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { - dropPosition: { x, y } + // Get the SaveAnimatedWEBP node + const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP') + const saveAnimatedWebpNode = saveNodes[0] + if (!saveAnimatedWebpNode) + throw new Error('SaveAnimatedWEBP node not found') + + // Simulate the graph executing + await comfyPage.page.evaluate( + ([loadId, saveId]) => { + // Set the output of the SaveAnimatedWEBP node to equal the loader node's image + window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId] + app.canvas.setDirty(true) + }, + [loadAnimatedWebpNode.id, saveAnimatedWebpNode.id] + ) + await expect( + comfyPage.page.locator('.dom-widget').locator('img') + ).toHaveCount(2) }) - await comfyPage.nextFrame() + } +) - // Get the SaveAnimatedWEBP node - const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP') - const saveAnimatedWebpNode = saveNodes[0] - if (!saveAnimatedWebpNode) - throw new Error('SaveAnimatedWEBP node not found') - - // Simulate the graph executing - await comfyPage.page.evaluate( - ([loadId, saveId]) => { - // Set the output of the SaveAnimatedWEBP node to equal the loader node's image - window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId] - app.canvas.setDirty(true) - }, - [loadAnimatedWebpNode.id, saveAnimatedWebpNode.id] - ) - await expect( - comfyPage.page.locator('.dom-widget').locator('img') - ).toHaveCount(2) - }) -}) - -test.describe('Load audio widget', () => { +test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can load audio', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/load_audio_widget') // Wait for the audio widget to be rendered in the DOM @@ -338,7 +348,7 @@ test.describe('Load audio widget', () => { }) }) -test.describe('Unserialized widgets', () => { +test.describe('Unserialized widgets', { tag: '@widget' }, () => { test('Unserialized widgets values do not mark graph as modified', async ({ comfyPage }) => { diff --git a/browser_tests/tests/workflowTabThumbnail.spec.ts b/browser_tests/tests/workflowTabThumbnail.spec.ts index 31b15067f..1f4d9464d 100644 --- a/browser_tests/tests/workflowTabThumbnail.spec.ts +++ b/browser_tests/tests/workflowTabThumbnail.spec.ts @@ -1,8 +1,9 @@ import { expect } from '@playwright/test' -import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' -test.describe('Workflow Tab Thumbnails', () => { +test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') diff --git a/docs/testing/store-testing.md b/docs/testing/store-testing.md index aec716c07..9b736fa36 100644 --- a/docs/testing/store-testing.md +++ b/docs/testing/store-testing.md @@ -18,7 +18,8 @@ Basic setup for testing Pinia stores: ```typescript // Example from: tests-ui/tests/store/workflowStore.test.ts -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useWorkflowStore } from '@/domains/workflow/ui/stores/workflowStore' @@ -27,8 +28,8 @@ describe('useWorkflowStore', () => { let store: ReturnType beforeEach(() => { - // Create a fresh pinia and activate it for each test - setActivePinia(createPinia()) + // Create a fresh testing pinia and activate it for each test + setActivePinia(createTestingPinia({ stubActions: false })) // Initialize the store store = useWorkflowStore() diff --git a/docs/testing/unit-testing.md b/docs/testing/unit-testing.md index a17592643..9be403659 100644 --- a/docs/testing/unit-testing.md +++ b/docs/testing/unit-testing.md @@ -11,6 +11,7 @@ This guide covers patterns and examples for unit testing utilities, composables, 5. [Mocking Utility Functions](#mocking-utility-functions) 6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle) 7. [Mocking Node Definitions](#mocking-node-definitions) +8. [Mocking Composables with Reactive State](#mocking-composables-with-reactive-state) ## Testing Vue Composables with Reactivity @@ -253,3 +254,79 @@ it('should validate node definition', () => { expect(validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toBeNull() }) ``` + +## Mocking Composables with Reactive State + +When mocking composables that return reactive refs, define the mock implementation inline in `vi.mock()`'s factory function. This ensures stable singleton instances across all test invocations. + +### Rules + +1. **Define mocks in the factory function** — Create `vi.fn()` and `ref()` instances directly inside `vi.mock()`, not in `beforeEach` +2. **Use singleton pattern** — The factory runs once; all calls to the composable return the same mock object +3. **Access mocks per-test** — Call the composable directly in each test to get the singleton instance rather than storing in a shared variable +4. **Wrap in `vi.mocked()` for type safety** — Use `vi.mocked(service.method).mockResolvedValue(...)` when configuring +5. **Rely on `vi.resetAllMocks()`** — Resets call counts without recreating instances; ref values may need manual reset if mutated + +### Pattern + +```typescript +// Example from: src/platform/updates/common/releaseStore.test.ts +import { ref } from 'vue' + +vi.mock('@/path/to/composable', () => { + const doSomething = vi.fn() + const isLoading = ref(false) + const error = ref(null) + return { + useMyComposable: () => ({ + doSomething, + isLoading, + error + }) + } +}) + +describe('MyStore', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should call the composable method', async () => { + const service = useMyComposable() + vi.mocked(service.doSomething).mockResolvedValue({ data: 'test' }) + + await store.initialize() + + expect(service.doSomething).toHaveBeenCalledWith(expectedArgs) + }) + + it('should handle errors from the composable', async () => { + const service = useMyComposable() + vi.mocked(service.doSomething).mockResolvedValue(null) + service.error.value = 'Something went wrong' + + await store.initialize() + + expect(store.error).toBe('Something went wrong') + }) +}) +``` + +### Anti-patterns + +```typescript +// ❌ Don't configure mock return values in beforeEach with shared variable +let mockService: { doSomething: Mock } +beforeEach(() => { + mockService = { doSomething: vi.fn() } + vi.mocked(useMyComposable).mockReturnValue(mockService) +}) + +// ❌ Don't auto-mock then override — reactive refs won't work correctly +vi.mock('@/path/to/composable') +vi.mocked(useMyComposable).mockReturnValue({ isLoading: ref(false) }) +``` + +``` + +``` diff --git a/package.json b/package.json index e1b00ac12..d872ac3ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@comfyorg/comfyui-frontend", - "version": "1.39.0", + "version": "1.39.4", "private": true, "description": "Official front-end implementation of ComfyUI", "homepage": "https://comfy.org", diff --git a/src/App.vue b/src/App.vue index b5e17500f..f843a7347 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,13 +1,11 @@
' - } + }, + ...stubs }, directives: { tooltip: () => {} @@ -91,6 +106,7 @@ function createTask(id: string, status: JobStatus): TaskItemImpl { describe('TopMenuSection', () => { beforeEach(() => { vi.resetAllMocks() + localStorage.clear() }) describe('authentication state', () => { @@ -151,7 +167,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? true : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) await nextTick() @@ -169,7 +185,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? false : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) const commandStore = useCommandStore(pinia) await wrapper.find('[data-testid="queue-overlay-toggle"]').trigger('click') @@ -185,7 +201,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? true : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) const sidebarTabStore = useSidebarTabStore(pinia) await wrapper.find('[data-testid="queue-overlay-toggle"]').trigger('click') @@ -199,7 +215,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? true : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) const sidebarTabStore = useSidebarTabStore(pinia) const toggleButton = wrapper.find('[data-testid="queue-overlay-toggle"]') @@ -210,6 +226,84 @@ describe('TopMenuSection', () => { expect(sidebarTabStore.activeSidebarTabId).toBe(null) }) + describe('inline progress summary', () => { + const configureSettings = ( + pinia: ReturnType, + qpoV2Enabled: boolean + ) => { + const settingStore = useSettingStore(pinia) + vi.mocked(settingStore.get).mockImplementation((key) => { + if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled + if (key === 'Comfy.UseNewMenu') return 'Top' + return undefined + }) + } + + it('renders inline progress summary when QPO V2 is enabled', async () => { + const pinia = createTestingPinia({ createSpy: vi.fn }) + configureSettings(pinia, true) + + const wrapper = createWrapper({ pinia }) + + await nextTick() + + expect( + wrapper.findComponent({ name: 'QueueInlineProgressSummary' }).exists() + ).toBe(true) + }) + + it('does not render inline progress summary when QPO V2 is disabled', async () => { + const pinia = createTestingPinia({ createSpy: vi.fn }) + configureSettings(pinia, false) + + const wrapper = createWrapper({ pinia }) + + await nextTick() + + expect( + wrapper.findComponent({ name: 'QueueInlineProgressSummary' }).exists() + ).toBe(false) + }) + + it('teleports inline progress summary when actionbar is floating', async () => { + localStorage.setItem('Comfy.MenuPosition.Docked', 'false') + const actionbarTarget = document.createElement('div') + document.body.appendChild(actionbarTarget) + const pinia = createTestingPinia({ createSpy: vi.fn }) + configureSettings(pinia, true) + const executionStore = useExecutionStore(pinia) + executionStore.activePromptId = 'prompt-1' + + const ComfyActionbarStub = defineComponent({ + name: 'ComfyActionbar', + setup(_, { emit }) { + onMounted(() => { + emit('update:progressTarget', actionbarTarget) + }) + return () => h('div') + } + }) + + const wrapper = createWrapper({ + pinia, + attachTo: document.body, + stubs: { + ComfyActionbar: ComfyActionbarStub, + QueueInlineProgressSummary: false + } + }) + + try { + await nextTick() + + expect(actionbarTarget.querySelector('[role="status"]')).not.toBeNull() + } finally { + wrapper.unmount() + actionbarTarget.remove() + } + }) + }) + it('disables the clear queue context menu item when no queued jobs exist', () => { const wrapper = createWrapper() const menu = wrapper.findComponent({ name: 'ContextMenu' }) diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 05149589c..a76d94f46 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -1,101 +1,130 @@ diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue index acb39e2e8..2ed71befc 100644 --- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -14,7 +14,12 @@ + { // Reset pagination when filters change watch( [ - searchQuery, + filteredTemplates, selectedNavItem, sortBy, selectedModels, diff --git a/src/components/dialog/content/setting/KeybindingPanel.vue b/src/components/dialog/content/setting/KeybindingPanel.vue index 03ca3df47..0bc15b9cf 100644 --- a/src/components/dialog/content/setting/KeybindingPanel.vue +++ b/src/components/dialog/content/setting/KeybindingPanel.vue @@ -150,13 +150,11 @@ import { useI18n } from 'vue-i18n' import SearchBox from '@/components/common/SearchBox.vue' import Button from '@/components/ui/button/Button.vue' -import { useKeybindingService } from '@/services/keybindingService' +import { KeyComboImpl } from '@/platform/keybindings/keyCombo' +import { KeybindingImpl } from '@/platform/keybindings/keybinding' +import { useKeybindingService } from '@/platform/keybindings/keybindingService' +import { useKeybindingStore } from '@/platform/keybindings/keybindingStore' import { useCommandStore } from '@/stores/commandStore' -import { - KeyComboImpl, - KeybindingImpl, - useKeybindingStore -} from '@/stores/keybindingStore' import { normalizeI18nKey } from '@/utils/formatUtil' import PanelTemplate from './PanelTemplate.vue' @@ -265,18 +263,15 @@ function cancelEdit() { } async function saveKeybinding() { - if (currentEditingCommand.value && newBindingKeyCombo.value) { - const updated = keybindingStore.updateKeybindingOnCommand( - new KeybindingImpl({ - commandId: currentEditingCommand.value.id, - combo: newBindingKeyCombo.value - }) - ) - if (updated) { - await keybindingService.persistUserKeybindings() - } - } + const commandId = currentEditingCommand.value?.id + const combo = newBindingKeyCombo.value cancelEdit() + if (!combo || commandId == undefined) return + + const updated = keybindingStore.updateKeybindingOnCommand( + new KeybindingImpl({ commandId, combo }) + ) + if (updated) await keybindingService.persistUserKeybindings() } async function resetKeybinding(commandData: ICommandData) { diff --git a/src/components/dialog/content/setting/UserPanel.vue b/src/components/dialog/content/setting/UserPanel.vue index 60bc6dba3..82985641d 100644 --- a/src/components/dialog/content/setting/UserPanel.vue +++ b/src/components/dialog/content/setting/UserPanel.vue @@ -63,14 +63,18 @@ {{ $t('auth.signOut.signOut') }} - + +
@@ -116,7 +120,6 @@ const { providerName, providerIcon, handleSignOut, - handleSignIn, - handleDeleteAccount + handleSignIn } = useCurrentUser() diff --git a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue index ded8cb18b..911a0f41b 100644 --- a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue +++ b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue @@ -13,7 +13,7 @@ import Tag from 'primevue/tag' import { computed } from 'vue' -import type { KeyComboImpl } from '@/stores/keybindingStore' +import type { KeyComboImpl } from '@/platform/keybindings/keyCombo' const { keyCombo, isModified = false } = defineProps<{ keyCombo: KeyComboImpl diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 08e7573a9..27c038076 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -149,7 +149,7 @@ import { app as comfyApp } from '@/scripts/app' import { ChangeTracker } from '@/scripts/changeTracker' import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets' import { useColorPaletteService } from '@/services/colorPaletteService' -import { newUserService } from '@/services/newUserService' +import { useNewUserService } from '@/services/useNewUserService' import { storeToRefs } from 'pinia' import { useBootstrapStore } from '@/stores/bootstrapStore' @@ -457,11 +457,9 @@ onMounted(async () => { // Register core settings immediately after settings are ready CORE_SETTINGS.forEach(settingStore.addSetting) - // Wait for both i18n and newUserService in parallel - // (newUserService only needs settings, not i18n) await Promise.all([ until(() => isI18nReady.value || !!i18nError.value).toBe(true), - newUserService().initializeIfNewUser(settingStore) + useNewUserService().initializeIfNewUser() ]) if (i18nError.value) { console.warn( diff --git a/src/components/graph/SelectionToolbox.test.ts b/src/components/graph/SelectionToolbox.test.ts index 441ca027f..bbbe9d786 100644 --- a/src/components/graph/SelectionToolbox.test.ts +++ b/src/components/graph/SelectionToolbox.test.ts @@ -13,6 +13,8 @@ import { createMockCanvas, createMockPositionable } from '@/utils/__tests__/litegraphTestUtils' +import * as litegraphUtil from '@/utils/litegraphUtil' +import * as nodeFilterUtil from '@/utils/nodeFilterUtil' function createMockExtensionService(): ReturnType { return { @@ -289,9 +291,8 @@ describe('SelectionToolbox', () => { ) }) - it('should show mask editor only for single image nodes', async () => { - const mockUtils = await import('@/utils/litegraphUtil') - const isImageNodeSpy = vi.spyOn(mockUtils, 'isImageNode') + it('should show mask editor only for single image nodes', () => { + const isImageNodeSpy = vi.spyOn(litegraphUtil, 'isImageNode') // Single image node isImageNodeSpy.mockReturnValue(true) @@ -307,9 +308,8 @@ describe('SelectionToolbox', () => { expect(wrapper2.find('.mask-editor-button').exists()).toBe(false) }) - it('should show Color picker button only for single Load3D nodes', async () => { - const mockUtils = await import('@/utils/litegraphUtil') - const isLoad3dNodeSpy = vi.spyOn(mockUtils, 'isLoad3dNode') + it('should show Color picker button only for single Load3D nodes', () => { + const isLoad3dNodeSpy = vi.spyOn(litegraphUtil, 'isLoad3dNode') // Single Load3D node isLoad3dNodeSpy.mockReturnValue(true) @@ -325,13 +325,9 @@ describe('SelectionToolbox', () => { expect(wrapper2.find('.load-3d-viewer-button').exists()).toBe(false) }) - it('should show ExecuteButton only when output nodes are selected', async () => { - const mockNodeFilterUtil = await import('@/utils/nodeFilterUtil') - const isOutputNodeSpy = vi.spyOn(mockNodeFilterUtil, 'isOutputNode') - const filterOutputNodesSpy = vi.spyOn( - mockNodeFilterUtil, - 'filterOutputNodes' - ) + it('should show ExecuteButton only when output nodes are selected', () => { + const isOutputNodeSpy = vi.spyOn(nodeFilterUtil, 'isOutputNode') + const filterOutputNodesSpy = vi.spyOn(nodeFilterUtil, 'filterOutputNodes') // With output node selected isOutputNodeSpy.mockReturnValue(true) diff --git a/src/components/graph/selectionToolbox/ExecuteButton.test.ts b/src/components/graph/selectionToolbox/ExecuteButton.test.ts index 6fe236d5b..370b96b1d 100644 --- a/src/components/graph/selectionToolbox/ExecuteButton.test.ts +++ b/src/components/graph/selectionToolbox/ExecuteButton.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue' +import { useSelectionState } from '@/composables/graph/useSelectionState' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCommandStore } from '@/stores/commandStore' @@ -47,7 +48,7 @@ describe('ExecuteButton', () => { } }) - beforeEach(async () => { + beforeEach(() => { // Set up Pinia with testing utilities setActivePinia( createTestingPinia({ @@ -71,10 +72,7 @@ describe('ExecuteButton', () => { vi.spyOn(commandStore, 'execute').mockResolvedValue() // Update the useSelectionState mock - const { useSelectionState } = vi.mocked( - await import('@/composables/graph/useSelectionState') - ) - useSelectionState.mockReturnValue({ + vi.mocked(useSelectionState).mockReturnValue({ selectedNodes: { value: mockSelectedNodes } diff --git a/src/components/helpcenter/HelpCenterPopups.vue b/src/components/helpcenter/HelpCenterPopups.vue index d4f059fd2..516ecb976 100644 --- a/src/components/helpcenter/HelpCenterPopups.vue +++ b/src/components/helpcenter/HelpCenterPopups.vue @@ -1,6 +1,6 @@ diff --git a/src/workbench/extensions/manager/components/manager/ManagerDialog.vue b/src/workbench/extensions/manager/components/manager/ManagerDialog.vue index 97f41da56..3800af058 100644 --- a/src/workbench/extensions/manager/components/manager/ManagerDialog.vue +++ b/src/workbench/extensions/manager/components/manager/ManagerDialog.vue @@ -130,16 +130,8 @@
selectedTab.value?.id === ManagerTab.Missing ) +// Map of tab IDs to their empty state i18n key suffixes +const tabEmptyStateKeys: Partial> = { + [ManagerTab.AllInstalled]: 'allInstalled', + [ManagerTab.UpdateAvailable]: 'updateAvailable', + [ManagerTab.Conflicting]: 'conflicting', + [ManagerTab.Workflow]: 'workflow', + [ManagerTab.Missing]: 'missing' +} + +// Empty state messages based on current tab and search state +const emptyStateTitle = computed(() => { + if (comfyManagerStore.error) return t('manager.errorConnecting') + if (searchQuery.value) return t('manager.noResultsFound') + + const tabId = selectedTab.value?.id as ManagerTab | undefined + const emptyStateKey = tabId ? tabEmptyStateKeys[tabId] : undefined + + return emptyStateKey + ? t(`manager.emptyState.${emptyStateKey}.title`) + : t('manager.noResultsFound') +}) + +const emptyStateMessage = computed(() => { + if (comfyManagerStore.error) return t('manager.tryAgainLater') + if (searchQuery.value) return t('manager.tryDifferentSearch') + + const tabId = selectedTab.value?.id as ManagerTab | undefined + const emptyStateKey = tabId ? tabEmptyStateKeys[tabId] : undefined + + return emptyStateKey + ? t(`manager.emptyState.${emptyStateKey}.message`) + : t('manager.tryDifferentSearch') +}) + const onClickWarningLink = () => { window.open( buildDocsUrl('/troubleshooting/custom-node-issues', { diff --git a/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts b/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts index e169ec9f0..c55b94dc2 100644 --- a/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts +++ b/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import Button from '@/components/ui/button/Button.vue' import { beforeEach, describe, expect, it, vi } from 'vitest' import { computed, ref } from 'vue' @@ -44,11 +45,11 @@ vi.mock( ) describe('NodeConflictDialogContent', () => { - let pinia: ReturnType + let pinia: ReturnType beforeEach(() => { vi.clearAllMocks() - pinia = createPinia() + pinia = createTestingPinia({ stubActions: false }) setActivePinia(pinia) // Reset mock data mockConflictData.value = [] diff --git a/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts b/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts index d57b25f1f..ff846b9f1 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts +++ b/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import PrimeVue from 'primevue/config' import Tooltip from 'primevue/tooltip' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -89,7 +89,7 @@ describe('PackVersionBadge', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n], + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n], directives: { tooltip: Tooltip }, diff --git a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts index 792983c22..ddd7a2962 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts +++ b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import Button from '@/components/ui/button/Button.vue' import PrimeVue from 'primevue/config' import Listbox from 'primevue/listbox' @@ -55,6 +55,8 @@ const mockNodePack = { const mockGetPackVersions = vi.fn() const mockInstallPack = vi.fn().mockResolvedValue(undefined) const mockCheckNodeCompatibility = vi.fn() +const mockIsPackInstalled = vi.fn(() => false) +const mockGetInstalledPackVersion = vi.fn(() => undefined) // Mock the registry service vi.mock('@/services/comfyRegistryService', () => ({ @@ -70,8 +72,8 @@ vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({ call: mockInstallPack, clear: vi.fn() }, - isPackInstalled: vi.fn(() => false), - getInstalledPackVersion: vi.fn(() => undefined) + isPackInstalled: mockIsPackInstalled, + getInstalledPackVersion: mockGetInstalledPackVersion })) })) @@ -98,6 +100,8 @@ describe('PackVersionSelectorPopover', () => { mockCheckNodeCompatibility .mockReset() .mockReturnValue({ hasConflict: false, conflicts: [] }) + mockIsPackInstalled.mockReset().mockReturnValue(false) + mockGetInstalledPackVersion.mockReset().mockReturnValue(undefined) }) const mountComponent = ({ @@ -115,7 +119,7 @@ describe('PackVersionSelectorPopover', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n], + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n], components: { Listbox, VerifiedIcon, diff --git a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue index f4a9896c3..030820d88 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue +++ b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue @@ -25,6 +25,7 @@ v-model="selectedVersion" option-label="label" option-value="value" + option-disabled="isInstalled" :options="processedVersionOptions" :highlight-on-select="false" class="max-h-[50vh] w-full rounded-md border-none shadow-none" @@ -71,7 +72,7 @@