diff --git a/.claude/commands/comprehensive-pr-review.md b/.claude/commands/comprehensive-pr-review.md index 84708564e..1b4047e78 100644 --- a/.claude/commands/comprehensive-pr-review.md +++ b/.claude/commands/comprehensive-pr-review.md @@ -67,9 +67,9 @@ This is critical for better file inspection: Use git locally for much faster analysis: -1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt` -2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt` -3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt` +1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt` +2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt` +3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt` ### Step 1.5: Create Analysis Cache diff --git a/.env_example b/.env_example index a6987ae26..520521fe0 100644 --- a/.env_example +++ b/.env_example @@ -33,4 +33,3 @@ DISABLE_VUE_PLUGINS=false # Algolia credentials required for developing with the new custom node manager. ALGOLIA_APP_ID=4E0RO38HS8 ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579 - diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 8decf07cd..06139b08a 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -7,3 +7,21 @@ c53f197de2a3e0fa66b16dedc65c131235c1c4b6 # Reorganize renderer components into domain-driven folder structure c8a83a9caede7bdb5f8598c5492b07d08c339d49 + +# Domain-driven design (DDD) refactors - September 2025 +# These commits reorganized the codebase into domain-driven architecture + +# [refactor] Improve renderer domain organization (#5552) +6349ceee6c0a57fc7992e85635def9b6e22eaeb2 + +# [refactor] Improve settings domain organization (#5550) +4c8c4a1ad4f53354f700a33ea1b95262aeda2719 + +# [refactor] Improve workflow domain organization (#5584) +ca312fd1eab540cc4ddc0e3d244d38b3858574f0 + +# [refactor] Move thumbnail functionality to renderer/core domain (#5586) +e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa + +# [refactor] Improve updates/notifications domain organization (#5590) +27ab355f9c73415dc39f4d3f512b02308f847801 diff --git a/.gitattributes b/.gitattributes index de05efbf4..bd0518cde 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,4 +13,4 @@ # Generated files src/types/comfyRegistryTypes.ts linguist-generated=true -src/types/generatedManagerTypes.ts linguist-generated=true +src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 3f4b93242..178bd4ee8 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -4,10 +4,25 @@ on: pull_request_target: types: [closed, labeled] branches: [main] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to backport' + required: true + type: string + force_rerun: + description: 'Force rerun even if backports exist' + required: false + type: boolean + default: false jobs: backport: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport') + if: > + (github.event_name == 'pull_request_target' && + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'needs-backport')) || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write @@ -15,6 +30,35 @@ jobs: issues: write steps: + - name: Validate inputs for manual triggers + if: github.event_name == 'workflow_dispatch' + run: | + # Validate PR number format + if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid PR number format. Must be a positive integer." + exit 1 + fi + + # Validate PR exists and is merged + if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then + echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible." + exit 1 + fi + + MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged') + if [ "$MERGED" != "true" ]; then + echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported." + exit 1 + fi + + # Validate PR has needs-backport label + if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then + echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label." + exit 1 + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository uses: actions/checkout@v4 with: @@ -29,7 +73,7 @@ jobs: id: check-existing env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | # Check for existing backport PRs for this PR number EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName') @@ -39,6 +83,13 @@ jobs: exit 0 fi + # For manual triggers with force_rerun, proceed anyway + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then + echo "skip=false" >> $GITHUB_OUTPUT + echo "::warning::Force rerun requested - existing backports will be updated" + exit 0 + fi + echo "Found existing backport PRs:" echo "$EXISTING_BACKPORTS" echo "skip=true" >> $GITHUB_OUTPUT @@ -50,8 +101,17 @@ jobs: run: | # Extract version labels (e.g., "1.24", "1.22") VERSIONS="" - LABELS='${{ toJSON(github.event.pull_request.labels) }}' - for label in $(echo "$LABELS" | jq -r '.[].name'); do + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # For manual triggers, get labels from the PR + LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name') + else + # For automatic triggers, extract from PR event + LABELS='${{ toJSON(github.event.pull_request.labels) }}' + LABELS=$(echo "$LABELS" | jq -r '.[].name') + fi + + for label in $LABELS; do # Match version labels like "1.24" (major.minor only) if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then # Validate the branch exists before adding to list @@ -75,12 +135,20 @@ jobs: if: steps.check-existing.outputs.skip != 'true' id: backport env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | FAILED="" SUCCESS="" + + # Get PR data for manual triggers + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + else + PR_TITLE="${{ github.event.pull_request.title }}" + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + fi for version in ${{ steps.versions.outputs.versions }}; do echo "::group::Backporting to core/${version}" @@ -133,11 +201,18 @@ jobs: if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success env: GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | - PR_TITLE="${{ github.event.pull_request.title }}" - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - + # Get PR data for manual triggers + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') + else + PR_TITLE="${{ github.event.pull_request.title }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + fi + for backport in ${{ steps.backport.outputs.success }}; do IFS=':' read -r version branch <<< "${backport}" @@ -166,9 +241,16 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit) + PR_NUMBER="${{ inputs.pr_number }}" + PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') + MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + else + PR_NUMBER="${{ github.event.pull_request.number }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + fi for failure in ${{ steps.backport.outputs.failed }}; do IFS=':' read -r version reason conflicts <<< "${failure}" diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index a5617c196..f46e9b7ac 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -32,11 +32,10 @@ jobs: with: repository: Comfy-Org/ComfyUI_frontend path: ComfyUI_frontend - - name: Checkout ComfyUI_devtools - uses: actions/checkout@v4 - with: - repository: Comfy-Org/ComfyUI_devtools - path: ComfyUI/custom_nodes/ComfyUI_devtools + - name: Copy ComfyUI_devtools from frontend repo + run: | + mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools + cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/ - name: Checkout custom node repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/publish-frontend-types.yaml b/.github/workflows/publish-frontend-types.yaml new file mode 100644 index 000000000..142a22a93 --- /dev/null +++ b/.github/workflows/publish-frontend-types.yaml @@ -0,0 +1,139 @@ +name: Publish Frontend Types + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 1.26.7)' + required: true + type: string + dist_tag: + description: 'npm dist-tag to use' + required: true + default: latest + type: string + ref: + description: 'Git ref to checkout (commit SHA, tag, or branch)' + required: false + type: string + workflow_call: + inputs: + version: + required: true + type: string + dist_tag: + required: false + type: string + default: latest + ref: + required: false + type: string + +concurrency: + group: publish-frontend-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }} + cancel-in-progress: false + +jobs: + publish_types_manual: + name: Publish @comfyorg/comfyui-frontend-types + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Validate inputs + shell: bash + run: | + set -euo pipefail + VERSION="${{ inputs.version }}" + SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' + if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then + echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2 + exit 1 + fi + + - name: Determine ref to checkout + id: resolve_ref + shell: bash + run: | + set -euo pipefail + REF="${{ inputs.ref }}" + VERSION="${{ inputs.version }}" + if [ -n "$REF" ]; then + if ! git check-ref-format --allow-onelevel "$REF"; then + echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2 + exit 1 + fi + echo "ref=$REF" >> "$GITHUB_OUTPUT" + else + echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout repository + uses: actions/checkout@v5 + with: + ref: ${{ steps.resolve_ref.outputs.ref }} + fetch-depth: 1 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 'lts/*' + cache: 'pnpm' + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: pnpm install --frozen-lockfile + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + + - name: Build types + run: pnpm build:types + + - name: Verify version matches input + id: verify + shell: bash + run: | + PKG_VERSION=$(node -p "require('./package.json').version") + TYPES_PKG_VERSION=$(node -p "require('./dist/package.json').version") + if [ "$PKG_VERSION" != "${{ inputs.version }}" ]; then + echo "Error: package.json version $PKG_VERSION does not match input ${{ inputs.version }}" >&2 + exit 1 + fi + if [ "$TYPES_PKG_VERSION" != "${{ inputs.version }}" ]; then + echo "Error: dist/package.json version $TYPES_PKG_VERSION does not match input ${{ inputs.version }}" >&2 + exit 1 + fi + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + + - name: Check if version already on npm + id: check_npm + shell: bash + run: | + set -euo pipefail + NAME=$(node -p "require('./dist/package.json').name") + VER="${{ steps.verify.outputs.version }}" + STATUS=0 + OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$? + if [ "$STATUS" -eq 0 ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish." + else + if echo "$OUTPUT" | grep -q "E404"; then + echo "exists=false" >> "$GITHUB_OUTPUT" + else + echo "::error title=Registry lookup failed::$OUTPUT" >&2 + exit "$STATUS" + fi + fi + + - name: Publish package + if: steps.check_npm.outputs.exists == 'false' + run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks + working-directory: dist + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8958ce147..c359e3da4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4 with: @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download dist artifact uses: actions/download-artifact@v4 with: @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download dist artifact uses: actions/download-artifact@v4 with: @@ -126,34 +126,8 @@ jobs: publish_types: needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - registry-url: https://registry.npmjs.org - - - name: Cache tool outputs - uses: actions/cache@v4 - with: - path: | - .cache - tsconfig.tsbuildinfo - dist - key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - types-tools-cache-${{ runner.os }}- - - - run: pnpm install --frozen-lockfile - - run: pnpm build:types - - name: Publish package - run: pnpm publish --access public - working-directory: dist - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + uses: ./.github/workflows/publish-frontend-types.yaml + with: + version: ${{ needs.build.outputs.version }} + ref: ${{ github.event.pull_request.merge_commit_sha }} + secrets: inherit diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index f8f6cf955..4f05a6d26 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -27,12 +27,10 @@ jobs: repository: 'Comfy-Org/ComfyUI_frontend' path: 'ComfyUI_frontend' - - name: Checkout ComfyUI_devtools - uses: actions/checkout@v4 - with: - repository: 'Comfy-Org/ComfyUI_devtools' - path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' + - name: Copy ComfyUI_devtools from frontend repo + run: | + mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools + cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/ - name: Install pnpm uses: pnpm/action-setup@v4 @@ -229,7 +227,13 @@ jobs: - name: Run Playwright tests (${{ matrix.browser }}) id: playwright - run: npx playwright test --project=${{ matrix.browser }} --reporter=html + run: | + # Run tests with both HTML and JSON reporters + PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ + npx playwright test --project=${{ matrix.browser }} \ + --reporter=list \ + --reporter=html \ + --reporter=json working-directory: ComfyUI_frontend - uses: actions/upload-artifact@v4 @@ -275,7 +279,12 @@ jobs: merge-multiple: true - name: Merge into HTML Report - run: npx playwright merge-reports --reporter html ./all-blob-reports + run: | + # Generate HTML report + npx playwright merge-reports --reporter=html ./all-blob-reports + # Generate JSON report separately with explicit output path + PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ + npx playwright merge-reports --reporter=json ./all-blob-reports working-directory: ComfyUI_frontend - name: Upload HTML report diff --git a/.github/workflows/update-electron-types.yaml b/.github/workflows/update-electron-types.yaml index 0dfcdea34..96f85f6b0 100644 --- a/.github/workflows/update-electron-types.yaml +++ b/.github/workflows/update-electron-types.yaml @@ -40,7 +40,7 @@ jobs: - name: Get new version id: get-version run: | - NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].version') + NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].dependencies."@comfyorg/comfyui-electron-types".version') echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT - name: Create Pull Request diff --git a/.gitignore b/.gitignore index 100bcd13e..e5bb5f107 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ dist-ssr *.local # Claude configuration .claude/*.local.json +.claude/*.local.md +.claude/*.local.txt CLAUDE.local.md # Editor directories and files @@ -44,6 +46,7 @@ components.d.ts tests-ui/data/* tests-ui/ComfyUI_examples tests-ui/workflows/examples +coverage/ # Browser tests /test-results/ @@ -51,7 +54,7 @@ tests-ui/workflows/examples /blob-report/ /playwright/.cache/ browser_tests/**/*-win32.png -browser-tests/local/ +browser_tests/local/ .env @@ -78,8 +81,8 @@ vite.config.mts.timestamp-*.mjs *storybook.log storybook-static - - +# MCP Servers +.playwright-mcp/* .nx/cache .nx/workspace-data diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 0429c3578..2efe5f966 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -9,7 +9,7 @@ module.exports = defineConfig({ entry: 'src/locales/en', entryLocale: 'en', output: 'src/locales', - outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'], + outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'], reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ae90f7051 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-workspace-root-check=true diff --git a/.storybook/main.ts b/.storybook/main.ts index a799ec143..aa6bb1fbd 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -15,21 +15,32 @@ const config: StorybookConfig = { async viteFinal(config) { // Use dynamic import to avoid CJS deprecation warning const { mergeConfig } = await import('vite') + const { default: tailwindcss } = await import('@tailwindcss/vite') // Filter out any plugins that might generate import maps if (config.plugins) { - config.plugins = config.plugins.filter((plugin: any) => { - if (plugin && plugin.name && plugin.name.includes('import-map')) { - return false - } - return true - }) + config.plugins = config.plugins + // Type guard: ensure we have valid plugin objects with names + .filter( + (plugin): plugin is NonNullable & { name: string } => { + return ( + plugin !== null && + plugin !== undefined && + typeof plugin === 'object' && + 'name' in plugin && + typeof plugin.name === 'string' + ) + } + ) + // Business logic: filter out import-map plugins + .filter((plugin) => !plugin.name.includes('import-map')) } return mergeConfig(config, { // Replace plugins entirely to avoid inheritance issues plugins: [ // Only include plugins we explicitly need for Storybook + tailwindcss(), Icons({ compiler: 'vue3', customCollections: { diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 747bbe802..bfe81f431 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,7 +1,7 @@ import { definePreset } from '@primevue/themes' import Aura from '@primevue/themes/aura' import { setup } from '@storybook/vue3' -import type { Preview } from '@storybook/vue3-vite' +import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite' import { createPinia } from 'pinia' import 'primeicons/primeicons.css' import PrimeVue from 'primevue/config' @@ -9,11 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice' import ToastService from 'primevue/toastservice' import Tooltip from 'primevue/tooltip' -import '../src/assets/css/style.css' -import { i18n } from '../src/i18n' -import '../src/lib/litegraph/public/css/litegraph.css' -import { useWidgetStore } from '../src/stores/widgetStore' -import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore' +import '@/assets/css/style.css' +import { i18n } from '@/i18n' +import '@/lib/litegraph/public/css/litegraph.css' const ComfyUIPreset = definePreset(Aura, { semantic: { @@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, { // Setup Vue app for Storybook setup((app) => { app.directive('tooltip', Tooltip) + + // Create Pinia instance const pinia = createPinia() + app.use(pinia) - - // Initialize stores - useColorPaletteStore(pinia) - useWidgetStore(pinia) - app.use(i18n) app.use(PrimeVue, { theme: { @@ -50,8 +46,8 @@ setup((app) => { app.use(ToastService) }) -// Dark theme decorator -export const withTheme = (Story: any, context: any) => { +// Theme and dialog decorator +export const withTheme = (Story: StoryFn, context: StoryContext) => { const theme = context.globals.theme || 'light' // Apply theme class to document root @@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => { document.body.classList.remove('dark-theme') } - return Story() + return Story(context.args, context) } const preview: Preview = { diff --git a/CODEOWNERS b/CODEOWNERS index 8d4e4a90f..cd1b4e508 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,17 +1,61 @@ -# Admins -* @Comfy-Org/comfy_frontend_devs +# Desktop/Electron +/src/types/desktop/ @webfiltered +/src/constants/desktopDialogs.ts @webfiltered +/src/constants/desktopMaintenanceTasks.ts @webfiltered +/src/stores/electronDownloadStore.ts @webfiltered +/src/extensions/core/electronAdapter.ts @webfiltered +/src/views/DesktopDialogView.vue @webfiltered +/src/components/install/ @webfiltered +/src/components/maintenance/ @webfiltered +/vite.electron.config.mts @webfiltered -# Maintainers -*.md @Comfy-Org/comfy_maintainer -/tests-ui/ @Comfy-Org/comfy_maintainer -/browser_tests/ @Comfy-Org/comfy_maintainer -/.env_example @Comfy-Org/comfy_maintainer +# Common UI Components +/src/components/chip/ @viva-jinyi +/src/components/card/ @viva-jinyi +/src/components/button/ @viva-jinyi +/src/components/input/ @viva-jinyi -# Translations (AIGODLIKE team + shinshin86) -/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer +# Topbar +/src/components/topbar/ @pythongosssss -# Load 3D extension -/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs +# Thumbnail +/src/renderer/core/thumbnail/ @pythongosssss -# Mask Editor extension -/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs +# Legacy UI +/scripts/ui/ @pythongosssss + +# Link rendering +/src/renderer/core/canvas/links/ @benceruleanlu + +# Node help system +/src/utils/nodeHelpUtil.ts @benceruleanlu +/src/stores/workspace/nodeHelpStore.ts @benceruleanlu +/src/services/nodeHelpService.ts @benceruleanlu + +# Selection toolbox +/src/components/graph/selectionToolbox/ @Myestery + +# Minimap +/src/renderer/extensions/minimap/ @jtydhr88 + +# Assets +/src/platform/assets/ @arjansingh + +# Workflow Templates +/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki +/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki + +# Mask Editor +/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp +/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp +/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp + +# 3D +/src/extensions/core/load3d.ts @jtydhr88 +/src/components/load3d/ @jtydhr88 + +# Manager +/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata + +# Translations +/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer diff --git a/browser_tests/README.md b/browser_tests/README.md index ede6a303a..ce0ed1f36 100644 --- a/browser_tests/README.md +++ b/browser_tests/README.md @@ -16,9 +16,14 @@ Without this flag, parallel tests will conflict and fail randomly. ### ComfyUI devtools -Clone to your `custom_nodes` directory. +ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory. _ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._ +For local development, copy the devtools files to your ComfyUI installation: +```bash +cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/ +``` + ### Node.js & Playwright Prerequisites Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver: @@ -51,14 +56,6 @@ TEST_COMFYUI_DIR=/path/to/your/ComfyUI ### Common Setup Issues -**Most tests require the new menu system** - Add to your test: - -```typescript -test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') -}) -``` - ### Release API Mocking By default, all tests mock the release API (`api.comfy.org/releases`) to prevent release notification popups from interfering with test execution. This is necessary because the release notifications can appear over UI elements and block test interactions. diff --git a/browser_tests/assets/vueNodes/simple-triple.json b/browser_tests/assets/vueNodes/simple-triple.json new file mode 100644 index 000000000..9b665191d --- /dev/null +++ b/browser_tests/assets/vueNodes/simple-triple.json @@ -0,0 +1 @@ +{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4} \ No newline at end of file diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index c32dd3937..19796f4c4 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -5,13 +5,14 @@ import dotenv from 'dotenv' import * as fs from 'fs' import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph' -import type { NodeId } from '../../src/schemas/comfyWorkflowSchema' +import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' import type { KeyCombo } from '../../src/schemas/keyBindingSchema' import type { useWorkspaceStore } from '../../src/stores/workspaceStore' import { NodeBadgeMode } from '../../src/types/nodeSource' import { ComfyActionbar } from '../helpers/actionbar' import { ComfyTemplates } from '../helpers/templates' import { ComfyMouse } from './ComfyMouse' +import { VueNodeHelpers } from './VueNodeHelpers' import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox' import { SettingDialog } from './components/SettingDialog' import { @@ -144,6 +145,7 @@ export class ComfyPage { public readonly templates: ComfyTemplates public readonly settingDialog: SettingDialog public readonly confirmDialog: ConfirmDialog + public readonly vueNodes: VueNodeHelpers /** Worker index to test user ID */ public readonly userIds: string[] = [] @@ -172,6 +174,7 @@ export class ComfyPage { this.templates = new ComfyTemplates(page) this.settingDialog = new SettingDialog(page, this) this.confirmDialog = new ConfirmDialog(page) + this.vueNodes = new VueNodeHelpers(page) } convertLeafToContent(structure: FolderStructure): FolderStructure { @@ -1421,7 +1424,7 @@ export class ComfyPage { } async closeDialog() { - await this.page.locator('.p-dialog-close-button').click() + await this.page.locator('.p-dialog-close-button').click({ force: true }) await expect(this.page.locator('.p-dialog')).toBeHidden() } @@ -1640,7 +1643,7 @@ export const comfyPageFixture = base.extend<{ try { await comfyPage.setupSettings({ - 'Comfy.UseNewMenu': 'Disabled', + 'Comfy.UseNewMenu': 'Top', // Hide canvas menu/info/selection toolbox by default. 'Comfy.Graph.CanvasInfo': false, 'Comfy.Graph.CanvasMenu': false, diff --git a/browser_tests/fixtures/UserSelectPage.ts b/browser_tests/fixtures/UserSelectPage.ts index 62a961375..ff0735e17 100644 --- a/browser_tests/fixtures/UserSelectPage.ts +++ b/browser_tests/fixtures/UserSelectPage.ts @@ -1,4 +1,5 @@ -import { Page, test as base } from '@playwright/test' +import type { Page } from '@playwright/test' +import { test as base } from '@playwright/test' export class UserSelectPage { constructor( diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts new file mode 100644 index 000000000..b51750299 --- /dev/null +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -0,0 +1,117 @@ +/** + * Vue Node Test Helpers + */ +import type { Locator, Page } from '@playwright/test' + +export class VueNodeHelpers { + constructor(private page: Page) {} + + /** + * Get locator for all Vue node components in the DOM + */ + get nodes(): Locator { + return this.page.locator('[data-node-id]') + } + + /** + * Get locator for selected Vue node components (using visual selection indicators) + */ + get selectedNodes(): Locator { + return this.page.locator( + '[data-node-id].outline-black, [data-node-id].outline-white' + ) + } + + /** + * Get locator for a Vue node by the node's title (displayed name in the header) + */ + getNodeByTitle(title: string): Locator { + return this.page.locator(`[data-node-id]`).filter({ hasText: title }) + } + + /** + * Get total count of Vue nodes in the DOM + */ + async getNodeCount(): Promise { + return await this.nodes.count() + } + + /** + * Get count of selected Vue nodes + */ + async getSelectedNodeCount(): Promise { + return await this.selectedNodes.count() + } + + /** + * Get all Vue node IDs currently in the DOM + */ + async getNodeIds(): Promise { + return await this.nodes.evaluateAll((nodes) => + nodes + .map((n) => n.getAttribute('data-node-id')) + .filter((id): id is string => id !== null) + ) + } + + /** + * Select a specific Vue node by ID + */ + async selectNode(nodeId: string): Promise { + await this.page.locator(`[data-node-id="${nodeId}"]`).click() + } + + /** + * Select multiple Vue nodes by IDs using Ctrl+click + */ + async selectNodes(nodeIds: string[]): Promise { + if (nodeIds.length === 0) return + + // Select first node normally + await this.selectNode(nodeIds[0]) + + // Add additional nodes with Ctrl+click + for (let i = 1; i < nodeIds.length; i++) { + await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({ + modifiers: ['Control'] + }) + } + } + + /** + * Clear all selections by clicking empty space + */ + async clearSelection(): Promise { + await this.page.mouse.click(50, 50) + } + + /** + * Delete selected Vue nodes using Delete key + */ + async deleteSelected(): Promise { + await this.page.locator('#graph-canvas').focus() + await this.page.keyboard.press('Delete') + } + + /** + * Delete selected Vue nodes using Backspace key + */ + async deleteSelectedWithBackspace(): Promise { + await this.page.locator('#graph-canvas').focus() + await this.page.keyboard.press('Backspace') + } + + /** + * Wait for Vue nodes to be rendered + */ + async waitForNodes(expectedCount?: number): Promise { + if (expectedCount !== undefined) { + await this.page.waitForFunction( + (count) => document.querySelectorAll('[data-node-id]').length >= count, + expectedCount + ) + } else { + await this.page.waitForSelector('[data-node-id]') + } + } +} diff --git a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts index 23dc104cf..fd40ca911 100644 --- a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts +++ b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' export class ComfyNodeSearchFilterSelectionPanel { constructor(public readonly page: Page) {} diff --git a/browser_tests/fixtures/components/SettingDialog.ts b/browser_tests/fixtures/components/SettingDialog.ts index afaf86154..e9040a3a9 100644 --- a/browser_tests/fixtures/components/SettingDialog.ts +++ b/browser_tests/fixtures/components/SettingDialog.ts @@ -1,6 +1,6 @@ -import { Page } from '@playwright/test' +import type { Page } from '@playwright/test' -import { ComfyPage } from '../ComfyPage' +import type { ComfyPage } from '../ComfyPage' export class SettingDialog { constructor( diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 7baaa1ef9..f3fbe42cf 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' class SidebarTab { constructor( diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts index 04a9117ce..6d0cd1fb3 100644 --- a/browser_tests/fixtures/components/Topbar.ts +++ b/browser_tests/fixtures/components/Topbar.ts @@ -1,4 +1,5 @@ -import { Locator, Page, expect } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' export class Topbar { private readonly menuLocator: Locator diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index c9bf88a91..4becc999c 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -1,6 +1,6 @@ import type { Page } from '@playwright/test' -import type { NodeId } from '../../../src/schemas/comfyWorkflowSchema' +import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema' import { ManageGroupNode } from '../../helpers/manageGroupNode' import type { ComfyPage } from '../ComfyPage' import type { Position, Size } from '../types' diff --git a/browser_tests/fixtures/ws.ts b/browser_tests/fixtures/ws.ts index e12c53465..f1ab1a538 100644 --- a/browser_tests/fixtures/ws.ts +++ b/browser_tests/fixtures/ws.ts @@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{ // so we can look it up to trigger messages const store: Record = ((window as any).__ws__ = {}) window.WebSocket = class extends window.WebSocket { - constructor() { - // @ts-expect-error - super(...arguments) + constructor( + ...rest: ConstructorParameters + ) { + super(...rest) store[this.url] = this } } diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts index 12033fce3..881ef11c4 100644 --- a/browser_tests/globalSetup.ts +++ b/browser_tests/globalSetup.ts @@ -1,4 +1,4 @@ -import { FullConfig } from '@playwright/test' +import type { FullConfig } from '@playwright/test' import dotenv from 'dotenv' import { backupPath } from './utils/backupUtils' diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts index 47bab3db9..aeed77294 100644 --- a/browser_tests/globalTeardown.ts +++ b/browser_tests/globalTeardown.ts @@ -1,4 +1,4 @@ -import { FullConfig } from '@playwright/test' +import type { FullConfig } from '@playwright/test' import dotenv from 'dotenv' import { restorePath } from './utils/backupUtils' diff --git a/browser_tests/helpers/fitToView.ts b/browser_tests/helpers/fitToView.ts new file mode 100644 index 000000000..af6c10e9d --- /dev/null +++ b/browser_tests/helpers/fitToView.ts @@ -0,0 +1,104 @@ +import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces' +import type { ComfyPage } from '../fixtures/ComfyPage' + +interface FitToViewOptions { + selectionOnly?: boolean + zoom?: number + padding?: number +} + +/** + * Instantly fits the canvas view to graph content without waiting for UI animation. + * + * Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented. + */ +export async function fitToViewInstant( + comfyPage: ComfyPage, + options: FitToViewOptions = {} +) { + const { selectionOnly = false, zoom = 0.75, padding = 10 } = options + + const rectangles = await comfyPage.page.evaluate< + ReadOnlyRect[] | null, + { selectionOnly: boolean } + >( + ({ selectionOnly }) => { + const app = window['app'] + if (!app?.canvas) return null + + const canvas = app.canvas + const items = (() => { + if (selectionOnly && canvas.selectedItems?.size) { + return Array.from(canvas.selectedItems) + } + try { + return Array.from(canvas.positionableItems ?? []) + } catch { + return [] + } + })() + + if (!items.length) return null + + const rects: ReadOnlyRect[] = [] + + for (const item of items) { + const rect = item?.boundingRect + if (!rect) continue + + const x = Number(rect[0]) + const y = Number(rect[1]) + const width = Number(rect[2]) + const height = Number(rect[3]) + + rects.push([x, y, width, height] as const) + } + + return rects.length ? rects : null + }, + { selectionOnly } + ) + + if (!rectangles || rectangles.length === 0) return + + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + + for (const [x, y, width, height] of rectangles) { + minX = Math.min(minX, Number(x)) + minY = Math.min(minY, Number(y)) + maxX = Math.max(maxX, Number(x) + Number(width)) + maxY = Math.max(maxY, Number(y) + Number(height)) + } + + const hasFiniteBounds = + Number.isFinite(minX) && + Number.isFinite(minY) && + Number.isFinite(maxX) && + Number.isFinite(maxY) + + if (!hasFiniteBounds) return + + const bounds: ReadOnlyRect = [ + minX - padding, + minY - padding, + maxX - minX + 2 * padding, + maxY - minY + 2 * padding + ] + + await comfyPage.page.evaluate( + ({ bounds, zoom }) => { + const app = window['app'] + if (!app?.canvas) return + + const canvas = app.canvas + canvas.ds.fitToBounds(bounds, { zoom }) + canvas.setDirty(true, true) + }, + { bounds, zoom } + ) + + await comfyPage.nextFrame() +} diff --git a/browser_tests/helpers/manageGroupNode.ts b/browser_tests/helpers/manageGroupNode.ts index a444a97c6..45010b979 100644 --- a/browser_tests/helpers/manageGroupNode.ts +++ b/browser_tests/helpers/manageGroupNode.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' export class ManageGroupNode { footer: Locator diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts index d659e125a..c690b8702 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -1,10 +1,10 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import path from 'path' -import { +import type { TemplateInfo, WorkflowTemplates -} from '../../src/types/workflowTemplateTypes' +} from '../../src/platform/workflow/templates/types/template' export class ComfyTemplates { readonly content: Locator diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index a504ea4fc..b23e4466d 100644 --- a/browser_tests/tests/actionbar.spec.ts +++ b/browser_tests/tests/actionbar.spec.ts @@ -29,9 +29,9 @@ test.describe('Actionbar', () => { // Intercept the prompt queue endpoint let promptNumber = 0 - comfyPage.page.route('**/api/prompt', async (route, req) => { + await comfyPage.page.route('**/api/prompt', async (route, req) => { await new Promise((r) => setTimeout(r, 100)) - route.fulfill({ + await route.fulfill({ status: 200, body: JSON.stringify({ prompt_id: promptNumber, diff --git a/browser_tests/tests/backgroundImageUpload.spec.ts b/browser_tests/tests/backgroundImageUpload.spec.ts index 24af9e8ac..7f3ed6a3d 100644 --- a/browser_tests/tests/backgroundImageUpload.spec.ts +++ b/browser_tests/tests/backgroundImageUpload.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Background Image Upload', () => { test.beforeEach(async ({ comfyPage }) => { // Reset the background image setting before each test diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts index 7a32833e4..8e39154f1 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -1,5 +1,5 @@ +import type { ComfyPage } from '../fixtures/ComfyPage' import { - ComfyPage, comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage' @@ -15,6 +15,10 @@ async function afterChange(comfyPage: ComfyPage) { }) } +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Change Tracker', () => { test.describe('Undo/Redo', () => { test.beforeEach(async ({ comfyPage }) => { diff --git a/browser_tests/tests/chatHistory.spec.ts b/browser_tests/tests/chatHistory.spec.ts index db3397514..c47a4d19b 100644 --- a/browser_tests/tests/chatHistory.spec.ts +++ b/browser_tests/tests/chatHistory.spec.ts @@ -1,7 +1,12 @@ -import { Page, expect } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + interface ChatHistoryEntry { prompt: string response: string diff --git a/browser_tests/tests/colorPalette.spec.ts b/browser_tests/tests/colorPalette.spec.ts index 901cce913..6dd53c194 100644 --- a/browser_tests/tests/colorPalette.spec.ts +++ b/browser_tests/tests/colorPalette.spec.ts @@ -3,6 +3,10 @@ import { expect } from '@playwright/test' import type { Palette } from '../../src/schemas/colorPaletteSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + const customColorPalettes: Record = { obsidian: { version: 102, diff --git a/browser_tests/tests/commands.spec.ts b/browser_tests/tests/commands.spec.ts index 4225ad228..e271f2e15 100644 --- a/browser_tests/tests/commands.spec.ts +++ b/browser_tests/tests/commands.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Keybindings', () => { test('Should execute command', async ({ comfyPage }) => { await comfyPage.registerCommand('TestCommand', () => { diff --git a/browser_tests/tests/copyPaste.spec.ts b/browser_tests/tests/copyPaste.spec.ts index 3bcee65f0..cabb849e8 100644 --- a/browser_tests/tests/copyPaste.spec.ts +++ b/browser_tests/tests/copyPaste.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Copy Paste', () => { test('Can copy and paste node', async ({ comfyPage }) => { await comfyPage.clickEmptyLatentNode() diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index cf2e5e6be..7459acf58 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,8 +1,13 @@ -import { Locator, expect } from '@playwright/test' +import type { Locator } from '@playwright/test' +import { expect } from '@playwright/test' import type { Keybinding } from '../../src/schemas/keyBindingSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Load workflow warning', () => { test('Should display a warning when loading a workflow with missing nodes', async ({ comfyPage diff --git a/browser_tests/tests/domWidget.spec.ts b/browser_tests/tests/domWidget.spec.ts index 91d53c407..6517b9170 100644 --- a/browser_tests/tests/domWidget.spec.ts +++ b/browser_tests/tests/domWidget.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('DOM Widget', () => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/collapsed_multiline') diff --git a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png index 90bc677fc..2b89be5c5 100644 Binary files a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png and b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png differ diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index 4adab98b6..075025a3a 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Execution', () => { test('Report error on unconnected slot', async ({ comfyPage }) => { await comfyPage.disconnectEdge() diff --git a/browser_tests/tests/extensionAPI.spec.ts b/browser_tests/tests/extensionAPI.spec.ts index 7711ccf3b..38f4a6c1d 100644 --- a/browser_tests/tests/extensionAPI.spec.ts +++ b/browser_tests/tests/extensionAPI.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { SettingParams } from '../../src/types/settingTypes' +import type { SettingParams } from '../../src/platform/settings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Topbar commands', () => { @@ -247,7 +247,7 @@ test.describe('Topbar commands', () => { test.describe('Dialog', () => { test('Should allow showing a prompt dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].extensionManager.dialog + void window['app'].extensionManager.dialog .prompt({ title: 'Test Prompt', message: 'Test Prompt Message' @@ -267,7 +267,7 @@ test.describe('Topbar commands', () => { comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].extensionManager.dialog + void window['app'].extensionManager.dialog .confirm({ title: 'Test Confirm', message: 'Test Confirm Message' @@ -284,7 +284,7 @@ test.describe('Topbar commands', () => { test('Should allow dismissing a dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { window['value'] = 'foo' - window['app'].extensionManager.dialog + void window['app'].extensionManager.dialog .confirm({ title: 'Test Confirm', message: 'Test Confirm Message' diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 73eb35f47..38286b399 100644 --- a/browser_tests/tests/featureFlags.spec.ts +++ b/browser_tests/tests/featureFlags.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Feature Flags', () => { test('Client and server exchange feature flags on connection', async ({ comfyPage diff --git a/browser_tests/tests/graph.spec.ts b/browser_tests/tests/graph.spec.ts index 25e166bab..cd89e92d5 100644 --- a/browser_tests/tests/graph.spec.ts +++ b/browser_tests/tests/graph.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Graph', () => { // Should be able to fix link input slot index after swap the input order // Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348 diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index 9ae090975..daa165fa4 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Graph Canvas Menu', () => { test.beforeEach(async ({ comfyPage }) => { // Set link render mode to spline to make sure it's not affected by other tests' diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png index f5ca69e54..2736a50c5 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png differ diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png index af9081d0a..fd72c2d0a 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png differ diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index 41b50224a..9a2310231 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -1,8 +1,13 @@ import { expect } from '@playwright/test' -import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Group Node', () => { test.describe('Node library sidebar', () => { const groupNodeName = 'DefautWorkflowGroupNode' diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index de46bca2e..2fc753490 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -1,12 +1,17 @@ -import { Locator, expect } from '@playwright/test' -import { Position } from '@vueuse/core' +import type { Locator } from '@playwright/test' +import { expect } from '@playwright/test' +import type { Position } from '@vueuse/core' import { type ComfyPage, comfyPageFixture as test, testComfySnapToGridGridSize } from '../fixtures/ComfyPage' -import { type NodeReference } from '../fixtures/utils/litegraphUtils' +import type { NodeReference } from '../fixtures/utils/litegraphUtils' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) test.describe('Item Interaction', () => { test('Can select/delete all items', async ({ comfyPage }) => { @@ -1012,6 +1017,8 @@ test.describe('Canvas Navigation', () => { test('Shift + mouse wheel should pan canvas horizontally', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning') + await comfyPage.page.click('canvas') await comfyPage.nextFrame() diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png index 57b6438ae..a9d0efb74 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png index a9d0efb74..57a92edc5 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png index 57b6438ae..e607294e3 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png differ diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts index ced293637..f4244ae66 100644 --- a/browser_tests/tests/keybindings.spec.ts +++ b/browser_tests/tests/keybindings.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Keybindings', () => { test('Should not trigger non-modifier keybinding when typing in input fields', async ({ comfyPage diff --git a/browser_tests/tests/litegraphEvent.spec.ts b/browser_tests/tests/litegraphEvent.spec.ts index 8d8f6c2e8..184943fe0 100644 --- a/browser_tests/tests/litegraphEvent.spec.ts +++ b/browser_tests/tests/litegraphEvent.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + function listenForEvent(): Promise { return new Promise((resolve) => { document.addEventListener('litegraph:canvas', (e) => resolve(e), { diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 678cb60f0..f091058d2 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Load Workflow in Media', () => { const fileNames = [ 'workflow.webp', diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts index 025347e4d..154ac3c16 100644 --- a/browser_tests/tests/lodThreshold.spec.ts +++ b/browser_tests/tests/lodThreshold.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('LOD Threshold', () => { test('Should switch to low quality mode at correct zoom threshold', async ({ comfyPage diff --git a/browser_tests/tests/nodeBadge.spec.ts b/browser_tests/tests/nodeBadge.spec.ts index 984dd6ea1..111efe29c 100644 --- a/browser_tests/tests/nodeBadge.spec.ts +++ b/browser_tests/tests/nodeBadge.spec.ts @@ -4,6 +4,10 @@ import type { ComfyApp } from '../../src/scripts/app' import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Node Badge', () => { test('Can add badge', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { diff --git a/browser_tests/tests/nodeDisplay.spec.ts b/browser_tests/tests/nodeDisplay.spec.ts index 2b76d4542..fdaae14bc 100644 --- a/browser_tests/tests/nodeDisplay.spec.ts +++ b/browser_tests/tests/nodeDisplay.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + // If an input is optional by node definition, it should be shown as // a hollow circle no matter what shape it was defined in the workflow JSON. test.describe('Optional input', () => { diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index 68ce7b8d5..764849286 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -46,7 +46,7 @@ test.describe('Node Help', () => { // Click the help button in the selection toolbox const helpButton = comfyPage.selectionToolbox.locator( - 'button:has(.pi-question-circle)' + 'button[data-testid="info-button"]' ) await expect(helpButton).toBeVisible() await helpButton.click() @@ -164,7 +164,7 @@ test.describe('Node Help', () => { // Click help button const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -194,7 +194,7 @@ test.describe('Node Help', () => { // Click help button const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -228,7 +228,7 @@ test.describe('Node Help', () => { await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -276,7 +276,7 @@ test.describe('Node Help', () => { await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -348,7 +348,7 @@ This is documentation for a custom node. } const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) if (await helpButton.isVisible()) { await helpButton.click() @@ -389,7 +389,7 @@ This is documentation for a custom node. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -456,7 +456,7 @@ This is English documentation. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -479,7 +479,7 @@ This is English documentation. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -522,7 +522,7 @@ This is English documentation. await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton.click() @@ -538,7 +538,7 @@ This is English documentation. // Click help button again const helpButton2 = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + '.selection-toolbox button[data-testid="info-button"]' ) await helpButton2.click() diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index 3c5e3cbe2..98ba33583 100644 --- a/browser_tests/tests/nodeSearchBox.spec.ts +++ b/browser_tests/tests/nodeSearchBox.spec.ts @@ -3,6 +3,10 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Node search box', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box') diff --git a/browser_tests/tests/noteNode.spec.ts b/browser_tests/tests/noteNode.spec.ts index 0f3d6a317..52dc57542 100644 --- a/browser_tests/tests/noteNode.spec.ts +++ b/browser_tests/tests/noteNode.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Note Node', () => { test('Can load node nodes', async ({ comfyPage }) => { await comfyPage.loadWorkflow('nodes/note_nodes') diff --git a/browser_tests/tests/primitiveNode.spec.ts b/browser_tests/tests/primitiveNode.spec.ts index 7fc408e8b..0584a3bec 100644 --- a/browser_tests/tests/primitiveNode.spec.ts +++ b/browser_tests/tests/primitiveNode.spec.ts @@ -3,6 +3,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Primitive Node', () => { test('Can load with correct size', async ({ comfyPage }) => { await comfyPage.loadWorkflow('primitive/primitive_node') diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index 4a390af96..7a54cae07 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' -import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Remote COMBO Widget', () => { const mockOptions = ['d', 'c', 'b', 'a'] @@ -190,7 +191,9 @@ test.describe('Remote COMBO Widget', () => { await comfyPage.page.keyboard.press('Control+A') await expect( - comfyPage.page.locator('.selection-toolbox .pi-refresh') + comfyPage.page.locator( + '.selection-toolbox button[data-testid="refresh-button"]' + ) ).toBeVisible() }) diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 89fdf38b2..0b2b1e0f6 100644 --- a/browser_tests/tests/rerouteNode.spec.ts +++ b/browser_tests/tests/rerouteNode.spec.ts @@ -40,6 +40,7 @@ test.describe('Reroute Node', () => { test.describe('LiteGraph Native Reroute Node', () => { test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) }) diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png index 6e63295b8..a9e9926bf 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts b/browser_tests/tests/rightClickMenu.spec.ts index db21ecd36..f7718122b 100644 --- a/browser_tests/tests/rightClickMenu.spec.ts +++ b/browser_tests/tests/rightClickMenu.spec.ts @@ -3,6 +3,10 @@ import { expect } from '@playwright/test' import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Canvas Right Click Menu', () => { test('Can add node', async ({ comfyPage }) => { await comfyPage.rightClickCanvas() diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png index 9443182e3..2755d74c5 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index a9a5fc9c2..6b8576982 100644 --- a/browser_tests/tests/selectionToolbox.spec.ts +++ b/browser_tests/tests/selectionToolbox.spec.ts @@ -4,6 +4,9 @@ import { comfyPageFixture } from '../fixtures/ComfyPage' const test = comfyPageFixture +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) const BLUE_COLOR = 'rgb(51, 51, 85)' const RED_COLOR = 'rgb(85, 51, 51)' diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png index 7aa22906b..96f6507e1 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png index 41bb283d9..af92221f3 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png index a9d9bafce..f9b9b012c 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts new file mode 100644 index 000000000..db6326152 --- /dev/null +++ b/browser_tests/tests/selectionToolboxSubmenus.spec.ts @@ -0,0 +1,181 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Selection Toolbox - More Options Submenus', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.nextFrame() + await comfyPage.selectNodes(['KSampler']) + await comfyPage.nextFrame() + }) + + const openMoreOptions = async (comfyPage: any) => { + const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') + if (ksamplerNodes.length === 0) { + throw new Error('No KSampler nodes found') + } + + // Drag the KSampler to the center of the screen + const nodePos = await ksamplerNodes[0].getPosition() + const viewportSize = comfyPage.page.viewportSize() + const centerX = viewportSize.width / 3 + const centerY = viewportSize.height / 2 + await comfyPage.dragAndDrop( + { x: nodePos.x, y: nodePos.y }, + { x: centerX, y: centerY } + ) + await comfyPage.nextFrame() + + await ksamplerNodes[0].click('title') + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(500) + + await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({ + timeout: 5000 + }) + + const moreOptionsBtn = comfyPage.page.locator( + '[data-testid="more-options-button"]' + ) + await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 }) + + await comfyPage.page.click('[data-testid="more-options-button"]') + + await comfyPage.nextFrame() + + const menuOptionsVisible = await comfyPage.page + .getByText('Rename') + .isVisible({ timeout: 2000 }) + .catch(() => false) + if (menuOptionsVisible) { + return + } + + await moreOptionsBtn.click({ force: true }) + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(2000) + + const menuOptionsVisibleAfterClick = await comfyPage.page + .getByText('Rename') + .isVisible({ timeout: 2000 }) + .catch(() => false) + if (menuOptionsVisibleAfterClick) { + return + } + + throw new Error('Could not open More Options menu - popover not showing') + } + + test('opens Node Info from More Options menu', async ({ comfyPage }) => { + await openMoreOptions(comfyPage) + const nodeInfoButton = comfyPage.page.getByText('Node Info', { + exact: true + }) + await expect(nodeInfoButton).toBeVisible() + await nodeInfoButton.click() + await comfyPage.nextFrame() + }) + + test('changes node shape via Shape submenu', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const initialShape = await nodeRef.getProperty('shape') + + await openMoreOptions(comfyPage) + await comfyPage.page.getByText('Shape', { exact: true }).click() + await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({ + timeout: 5000 + }) + await comfyPage.page.getByText('Box', { exact: true }).click() + await comfyPage.nextFrame() + + const newShape = await nodeRef.getProperty('shape') + expect(newShape).not.toBe(initialShape) + expect(newShape).toBe(1) + }) + + test('changes node color via Color submenu swatch', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const initialColor = await nodeRef.getProperty('color') + + await openMoreOptions(comfyPage) + await comfyPage.page.getByText('Color', { exact: true }).click() + const blueSwatch = comfyPage.page.locator('[title="Blue"]') + await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 }) + await blueSwatch.first().click() + await comfyPage.nextFrame() + + const newColor = await nodeRef.getProperty('color') + expect(newColor).toBe('#223') + if (initialColor) { + expect(newColor).not.toBe(initialColor) + } + }) + + test('renames a node using Rename action', async ({ comfyPage }) => { + const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + await openMoreOptions(comfyPage) + await comfyPage.page + .getByText('Rename', { exact: true }) + .click({ force: true }) + const input = comfyPage.page.locator( + '.group-title-editor.node-title-editor .editable-text input' + ) + await expect(input).toBeVisible() + await input.fill('RenamedNode') + await input.press('Enter') + await comfyPage.nextFrame() + const newTitle = await nodeRef.getProperty('title') + expect(newTitle).toBe('RenamedNode') + }) + + test('closes More Options menu when clicking outside', async ({ + comfyPage + }) => { + await openMoreOptions(comfyPage) + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).toBeVisible({ timeout: 5000 }) + + await comfyPage.page + .locator('#graph-canvas') + .click({ position: { x: 0, y: 50 }, force: true }) + await comfyPage.nextFrame() + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).not.toBeVisible() + }) + + test('closes More Options menu when clicking the button again (toggle)', async ({ + comfyPage + }) => { + await openMoreOptions(comfyPage) + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).toBeVisible({ timeout: 5000 }) + + await comfyPage.page.evaluate(() => { + const btn = document.querySelector('[data-testid="more-options-button"]') + if (btn) { + const event = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + detail: 1 + }) + btn.dispatchEvent(event) + } + }) + await comfyPage.nextFrame() + await comfyPage.page.waitForTimeout(500) + + await expect( + comfyPage.page.getByText('Rename', { exact: true }) + ).not.toBeVisible() + }) +}) diff --git a/browser_tests/tests/sidebar/queue.spec.ts b/browser_tests/tests/sidebar/queue.spec.ts index 2d9dd10ba..39e2ced6e 100644 --- a/browser_tests/tests/sidebar/queue.spec.ts +++ b/browser_tests/tests/sidebar/queue.spec.ts @@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => { comfyPage }) => { await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(firstImage) + ).toBeVisible() }) test('maintains active gallery item when new tasks are added', async ({ @@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => { const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage) await newTask.waitFor({ state: 'visible' }) // The active gallery item should still be the initial image - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(firstImage) + ).toBeVisible() }) test.describe('Gallery navigation', () => { @@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => { delay: 256 }) await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(end) + ).toBeVisible() }) }) }) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index f2c2e2bb5..9141e9135 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -1,4 +1,5 @@ -import { Page, expect } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png index 719ec65e4..ce88325aa 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png index b880fca84..bf1d18934 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png differ diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index 69a40ced9..a817616f8 100644 --- a/browser_tests/tests/useSettingSearch.spec.ts +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Settings Search functionality', () => { test.beforeEach(async ({ comfyPage }) => { // Register test settings to verify hidden/deprecated filtering diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts index d85f18723..eee6fda92 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { SystemStats } from '../../src/schemas/apiSchema' +import type { SystemStats } from '../../src/schemas/apiSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Version Mismatch Warnings', () => { @@ -106,6 +106,11 @@ test.describe('Version Mismatch Warnings', () => { const dismissButton = warningToast.getByRole('button', { name: 'Close' }) await dismissButton.click() + // Wait for the dismissed state to be persisted + await comfyPage.page.waitForFunction( + () => !!localStorage.getItem('comfy.versionMismatch.dismissals') + ) + // Reload the page, keeping local storage await comfyPage.setup({ clearStorage: false }) diff --git a/browser_tests/tests/vueNodes/NodeHeader.spec.ts b/browser_tests/tests/vueNodes/NodeHeader.spec.ts index 7a8ae5dd2..336e2672d 100644 --- a/browser_tests/tests/vueNodes/NodeHeader.spec.ts +++ b/browser_tests/tests/vueNodes/NodeHeader.spec.ts @@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures' test.describe('NodeHeader', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled') + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) diff --git a/browser_tests/tests/vueNodes/deleteKeyInteraction.spec.ts b/browser_tests/tests/vueNodes/deleteKeyInteraction.spec.ts new file mode 100644 index 000000000..51b52e7ce --- /dev/null +++ b/browser_tests/tests/vueNodes/deleteKeyInteraction.spec.ts @@ -0,0 +1,145 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Vue Nodes - Delete Key Interaction', () => { + test.beforeEach(async ({ comfyPage }) => { + // Enable Vue nodes rendering + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.setup() + }) + + test('Can select all and delete Vue nodes with Delete key', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + + // Get initial Vue node count + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(initialNodeCount).toBeGreaterThan(0) + + // Select all Vue nodes + await comfyPage.ctrlA() + + // Verify all Vue nodes are selected + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(initialNodeCount) + + // Delete with Delete key + await comfyPage.vueNodes.deleteSelected() + + // Verify all Vue nodes were deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(0) + }) + + test('Can select specific Vue node and delete it', async ({ comfyPage }) => { + await comfyPage.vueNodes.waitForNodes() + + // Get initial Vue node count + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(initialNodeCount).toBeGreaterThan(0) + + // Get first Vue node ID and select it + const nodeIds = await comfyPage.vueNodes.getNodeIds() + await comfyPage.vueNodes.selectNode(nodeIds[0]) + + // Verify selection + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(1) + + // Delete with Delete key + await comfyPage.vueNodes.deleteSelected() + + // Verify one Vue node was deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(initialNodeCount - 1) + }) + + test('Can select and delete Vue node with Backspace key', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + + // Select first Vue node + const nodeIds = await comfyPage.vueNodes.getNodeIds() + await comfyPage.vueNodes.selectNode(nodeIds[0]) + + // Delete with Backspace key instead of Delete + await comfyPage.vueNodes.deleteSelectedWithBackspace() + + // Verify Vue node was deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(initialNodeCount - 1) + }) + + test('Delete key does not delete node when typing in Vue node widgets', async ({ + comfyPage + }) => { + const initialNodeCount = await comfyPage.getGraphNodesCount() + + // Find a text input widget in a Vue node + const textWidget = comfyPage.page + .locator('input[type="text"], textarea') + .first() + + // Click on text widget to focus it + await textWidget.click() + await textWidget.fill('test text') + + // Press Delete while focused on widget - should delete text, not node + await textWidget.press('Delete') + + // Node count should remain the same + const finalNodeCount = await comfyPage.getGraphNodesCount() + expect(finalNodeCount).toBe(initialNodeCount) + }) + + test('Delete key does not delete node when nothing is selected', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + + // Ensure no Vue nodes are selected + await comfyPage.vueNodes.clearSelection() + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(0) + + // Press Delete key - should not crash and should handle gracefully + await comfyPage.page.keyboard.press('Delete') + + // Vue node count should remain the same + const nodeCount = await comfyPage.vueNodes.getNodeCount() + expect(nodeCount).toBeGreaterThan(0) + }) + + test('Can multi-select with Ctrl+click and delete multiple Vue nodes', async ({ + comfyPage + }) => { + await comfyPage.vueNodes.waitForNodes() + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + + // Multi-select first two Vue nodes using Ctrl+click + const nodeIds = await comfyPage.vueNodes.getNodeIds() + const nodesToSelect = nodeIds.slice(0, 2) + await comfyPage.vueNodes.selectNodes(nodesToSelect) + + // Verify expected nodes are selected + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(nodesToSelect.length) + + // Delete selected Vue nodes + await comfyPage.vueNodes.deleteSelected() + + // Verify expected nodes were deleted + const finalNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(finalNodeCount).toBe(initialNodeCount - nodesToSelect.length) + }) +}) diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/linkInteraction.spec.ts new file mode 100644 index 000000000..d6b1bccc1 --- /dev/null +++ b/browser_tests/tests/vueNodes/linkInteraction.spec.ts @@ -0,0 +1,221 @@ +import type { Locator } from '@playwright/test' + +import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier' +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../fixtures/ComfyPage' +import { fitToViewInstant } from '../../helpers/fitToView' + +async function getCenter(locator: Locator): Promise<{ x: number; y: number }> { + const box = await locator.boundingBox() + if (!box) throw new Error('Slot bounding box not available') + return { + x: box.x + box.width / 2, + y: box.y + box.height / 2 + } +} + +test.describe('Vue Node Link Interaction', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.vueNodes.waitForNodes() + await fitToViewInstant(comfyPage) + }) + + test('should show a link dragging out from a slot when dragging on a slot', async ({ + comfyPage, + comfyMouse + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + + const samplerNode = samplerNodes[0] + const outputSlot = await samplerNode.getOutput(0) + await outputSlot.removeLinks() + await comfyPage.nextFrame() + + const slotKey = getSlotKey(String(samplerNode.id), 0, false) + const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`) + await expect(slotLocator).toBeVisible() + + const start = await getCenter(slotLocator) + const canvasBox = await comfyPage.canvas.boundingBox() + if (!canvasBox) throw new Error('Canvas bounding box not available') + + // Arbitrary value + const dragTarget = { + x: start.x + 180, + y: start.y - 140 + } + + await comfyMouse.move(start) + await comfyMouse.drag(dragTarget) + await comfyPage.nextFrame() + + try { + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-dragging-link.png' + ) + } finally { + await comfyMouse.drop() + } + }) + + test('should create a link when dropping on a compatible slot', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + expect(vaeNodes.length).toBeGreaterThan(0) + const vaeNode = vaeNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const vaeInput = await vaeNode.getInput(0) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(1) + expect(await vaeInput.getLinkCount()).toBe(1) + + const linkDetails = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph ?? app?.graph + if (!graph) return null + + const source = graph.getNodeById(sourceId) + if (!source) return null + + const linkId = source.outputs[0]?.links?.[0] + if (linkId == null) return null + + const link = graph.links[linkId] + if (!link) return null + + return { + originId: link.origin_id, + originSlot: link.origin_slot, + targetId: link.target_id, + targetSlot: link.target_slot + } + }, samplerNode.id) + + expect(linkDetails).not.toBeNull() + expect(linkDetails).toMatchObject({ + originId: samplerNode.id, + originSlot: 0, + targetId: vaeNode.id, + targetSlot: 0 + }) + }) + + test('should not create a link when slot types are incompatible', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + expect(clipNodes.length).toBeGreaterThan(0) + const clipNode = clipNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const clipInput = await clipNode.getInput(0) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(clipNode.id), 0, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(0) + expect(await clipInput.getLinkCount()).toBe(0) + + const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph ?? app?.graph + if (!graph) return 0 + + const source = graph.getNodeById(sourceId) + if (!source) return 0 + + return source.outputs[0]?.links?.length ?? 0 + }, samplerNode.id) + + expect(graphLinkCount).toBe(0) + }) + + test('should not create a link when dropping onto a slot on the same node', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const samplerInput = await samplerNode.getInput(3) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(0) + expect(await samplerInput.getLinkCount()).toBe(0) + + const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph ?? app?.graph + if (!graph) return 0 + + const source = graph.getNodeById(sourceId) + if (!source) return 0 + + return source.outputs[0]?.links?.length ?? 0 + }, samplerNode.id) + + expect(graphLinkCount).toBe(0) + }) +}) diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png new file mode 100644 index 000000000..78e331b3c Binary files /dev/null and b/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/lod.spec.ts b/browser_tests/tests/vueNodes/lod.spec.ts new file mode 100644 index 000000000..2ed598ef8 --- /dev/null +++ b/browser_tests/tests/vueNodes/lod.spec.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Vue Nodes - LOD', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + await comfyPage.loadWorkflow('default') + }) + + test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => { + await comfyPage.vueNodes.waitForNodes() + + const initialNodeCount = await comfyPage.vueNodes.getNodeCount() + expect(initialNodeCount).toBeGreaterThan(0) + + await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png') + + const vueNodesContainer = comfyPage.vueNodes.nodes + const textboxesInNodes = vueNodesContainer.getByRole('textbox') + const buttonsInNodes = vueNodesContainer.getByRole('button') + + await expect(textboxesInNodes.first()).toBeVisible() + await expect(buttonsInNodes.first()).toBeVisible() + + await comfyPage.zoom(120, 10) + await comfyPage.nextFrame() + + await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png') + + await expect(textboxesInNodes.first()).toBeHidden() + await expect(buttonsInNodes.first()).toBeHidden() + + await comfyPage.zoom(-120, 10) + await comfyPage.nextFrame() + + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-nodes-lod-inactive.png' + ) + await expect(textboxesInNodes.first()).toBeVisible() + await expect(buttonsInNodes.first()).toBeVisible() + }) +}) diff --git a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png new file mode 100644 index 000000000..744e40594 Binary files /dev/null and b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png new file mode 100644 index 000000000..5dfa61c19 Binary files /dev/null and b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png new file mode 100644 index 000000000..8091e5d03 Binary files /dev/null and b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts b/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts new file mode 100644 index 000000000..591c1d307 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts @@ -0,0 +1,51 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + +test.describe('Vue Node Selection', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + const modifiers = [ + { key: 'Control', name: 'ctrl' }, + { key: 'Shift', name: 'shift' } + ] as const + + for (const { key: modifier, name } of modifiers) { + test(`should allow selecting multiple nodes with ${name}+click`, async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + await comfyPage.page.getByText('Empty Latent Image').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2) + + await comfyPage.page.getByText('KSampler').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3) + }) + + test(`should allow de-selecting nodes with ${name}+click`, async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + await comfyPage.page.getByText('Load Checkpoint').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0) + }) + } +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts new file mode 100644 index 000000000..74ec17cc9 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -0,0 +1,45 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const BYPASS_HOTKEY = 'Control+b' +const BYPASS_CLASS = /before:bg-bypass\/60/ + +test.describe('Vue Node Bypass', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling bypass on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + }) + + test('should allow toggling bypass on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + await expect(ksamplerNode).toHaveClass(BYPASS_CLASS) + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts new file mode 100644 index 000000000..f4f8e10fe --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts @@ -0,0 +1,32 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const ERROR_CLASS = /border-error/ + +test.describe('Vue Node Error', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should display error state when node is missing (node from workflow is not installed)', async ({ + comfyPage + }) => { + await comfyPage.setup() + await comfyPage.loadWorkflow('missing/missing_nodes') + + // Close missing nodes warning dialog + await comfyPage.page.getByRole('button', { name: 'Close' }).click() + await comfyPage.page.waitForSelector('.comfy-missing-nodes', { + state: 'hidden' + }) + + // Expect error state on missing unknown node + const unknownNode = comfyPage.page.locator('[data-node-id]').filter({ + hasText: 'UNKNOWN NODE' + }) + await expect(unknownNode).toHaveClass(ERROR_CLASS) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts new file mode 100644 index 000000000..37dcfd37b --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -0,0 +1,45 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const MUTE_HOTKEY = 'Control+m' +const MUTE_CLASS = /opacity-50/ + +test.describe('Vue Node Mute', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling mute on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await expect(checkpointNode).toHaveClass(MUTE_CLASS) + + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) + }) + + test('should allow toggling mute on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).toHaveClass(MUTE_CLASS) + await expect(ksamplerNode).toHaveClass(MUTE_CLASS) + + await comfyPage.page.keyboard.press(MUTE_HOTKEY) + await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) + await expect(ksamplerNode).not.toHaveClass(MUTE_CLASS) + }) +}) diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index b23faabfc..3b9c05784 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -2,6 +2,10 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') +}) + test.describe('Combo text widget', () => { test('Truncates text when resized', async ({ comfyPage }) => { await comfyPage.resizeLoadCheckpointNode(0.2, 1) @@ -264,7 +268,13 @@ test.describe('Animated image widget', () => { expect(filename).toContain('animated_webp.webp') }) - test('Can preview saved animated webp image', async ({ comfyPage }) => { + // FIXME: This test keeps flip-flopping because it relies on animated webp timing, + // which is inherently unreliable in CI environments. The test asset is an animated + // webp with 2 frames, and the test depends on animation frame timing to verify that + // animated webp images are properly displayed (as opposed to being treated as static webp). + // While the underlying functionality works (animated webp are correctly distinguished + // from static webp), the test is flaky due to timing dependencies with webp animation frames. + test.fixme('Can preview saved animated webp image', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/save_animated_webp') // Get position of the load animated webp node @@ -312,6 +322,9 @@ test.describe('Animated image widget', () => { test.describe('Load audio widget', () => { test('Can load audio', async ({ comfyPage }) => { await comfyPage.loadWorkflow('widgets/load_audio_widget') + // Wait for the audio widget to be rendered in the DOM + await comfyPage.page.waitForSelector('.comfy-audio', { state: 'attached' }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png') }) }) diff --git a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png deleted file mode 100644 index 474fbb555..000000000 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png and /dev/null differ diff --git a/browser_tests/tsconfig.json b/browser_tests/tsconfig.json new file mode 100644 index 000000000..391298333 --- /dev/null +++ b/browser_tests/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* Test files should not be compiled */ + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts", + ] +} diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 3f795a219..5b7f4bec4 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -1,5 +1,5 @@ import path from 'path' -import { Plugin } from 'vite' +import type { Plugin } from 'vite' interface ShimResult { code: string diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts index 80ccb6c9f..bbbf14c2c 100644 --- a/build/plugins/generateImportMapPlugin.ts +++ b/build/plugins/generateImportMapPlugin.ts @@ -1,7 +1,7 @@ import glob from 'fast-glob' import fs from 'fs-extra' import { dirname, join } from 'node:path' -import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite' +import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite' interface ImportMapSource { name: string diff --git a/build/tsconfig.json b/build/tsconfig.json new file mode 100644 index 000000000..1c24810a8 --- /dev/null +++ b/build/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* Build scripts configuration */ + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 000000000..5526f900d --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "new-york", + "typescript": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/assets/css/style.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "composables": "@/composables", + "utils": "@/utils", + "ui": "@/components/ui", + "lib": "@/lib" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/docs/adr/0004-fork-primevue-ui-library.md b/docs/adr/0004-fork-primevue-ui-library.md new file mode 100644 index 000000000..02bd18736 --- /dev/null +++ b/docs/adr/0004-fork-primevue-ui-library.md @@ -0,0 +1,62 @@ +# 4. Fork PrimeVue UI Library + +Date: 2025-08-27 + +## Status + +Rejected + +## Context + +ComfyUI's frontend requires modifications to PrimeVue components that cannot be achieved through the library's customization APIs. Two specific technical incompatibilities have been identified with the transform-based canvas architecture: + +**Screen Coordinate Hit-Testing Conflicts:** +- PrimeVue components use `getBoundingClientRect()` for screen coordinate calculations that don't account for CSS transforms +- The Slider component directly uses raw `pageX/pageY` coordinates ([lines 102-103](https://github.com/primefaces/primevue/blob/master/packages/primevue/src/slider/Slider.vue#L102-L103)) without transform-aware positioning +- This breaks interaction in transformed coordinate spaces where screen coordinates don't match logical element positions + +**Virtual Canvas Scroll Interference:** +- LiteGraph's infinite canvas uses scroll coordinates semantically for graph navigation via the `DragAndScale` coordinate system +- PrimeVue overlay components automatically trigger `scrollIntoView` behavior which interferes with this virtual positioning +- This issue is documented in [PrimeVue discussion #4270](https://github.com/orgs/primefaces/discussions/4270) where the feature request was made to disable this behavior + +**Historical Overlay Issues:** +- Previous z-index positioning conflicts required manual workarounds (commit `6d4eafb0`) where PrimeVue Dialog components needed `autoZIndex: false` and custom mask styling, later resolved by removing PrimeVue's automatic z-index management entirely + +**Minimal Update Overhead:** +- Analysis of git history shows only 2 PrimeVue version updates in 2+ years, indicating that upstream sync overhead is negligible for this project + +**Future Interaction System Requirements:** +- The ongoing canvas architecture evolution will require more granular control over component interaction and event handling as the transform-based system matures +- Predictable need for additional component modifications beyond current identified issues + +## Decision + +We will **NOT** fork PrimeVue. After evaluation, forking was determined to be unnecessarily complex and costly. + +**Rationale for Rejection:** + +- **Significant Implementation Complexity**: PrimeVue is structured as a monorepo ([primefaces/primevue](https://github.com/primefaces/primevue)) with significant code in a separate monorepo ([PrimeUIX](https://github.com/primefaces/primeuix)). Forking would require importing both repositories whole and selectively pruning or exempting components from our workspace tooling, adding substantial complexity. + +- **Alternative Solutions Available**: The modifications we identified (e.g., scroll interference issues, coordinate system conflicts) have less costly solutions that don't require maintaining a full fork. For example, coordinate issues could be addressed through event interception and synthetic event creation with scaled values. + +- **Maintenance Burden**: Ongoing maintenance and upgrades would be very painful, requiring manual conflict resolution and keeping pace with upstream changes across multiple repositories. + +- **Limited Tooling Support**: There isn't adequate tooling that provides the granularity needed to cleanly manage a PrimeVue fork within our existing infrastructure. + +## Consequences + +### Alternative Approach + +- **Use PrimeVue as External Dependency**: Continue using PrimeVue as a standard npm dependency +- **Targeted Workarounds**: Implement specific solutions for identified issues (coordinate system conflicts, scroll interference) without forking the entire library +- **Selective Component Replacement**: Use libraries like shadcn/ui to replace specific problematic PrimeVue components and adjust them to match our design system +- **Upstream Engagement**: Continue engaging with PrimeVue community for feature requests and bug reports +- **Maintain Flexibility**: Preserve ability to upgrade PrimeVue versions without fork maintenance overhead + +## Notes + +- Technical issues documented in the Context section remain valid concerns +- Solutions will be pursued through targeted fixes rather than wholesale forking +- Future re-evaluation possible if PrimeVue's architecture significantly changes or if alternative tooling becomes available +- This decision prioritizes maintainability and development velocity over maximum customization control diff --git a/docs/adr/README.md b/docs/adr/README.md index 00e50a639..5f6e5c2cf 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -13,6 +13,7 @@ An Architecture Decision Record captures an important architectural decision mad | [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 | | [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 | | [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 | +| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 | ## Creating a New ADR diff --git a/eslint.config.js b/eslint.config.ts similarity index 65% rename from eslint.config.js rename to eslint.config.ts index 7e3248b20..ab3bf09f5 100644 --- a/eslint.config.js +++ b/eslint.config.ts @@ -5,13 +5,14 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' import storybook from 'eslint-plugin-storybook' import unusedImports from 'eslint-plugin-unused-imports' import pluginVue from 'eslint-plugin-vue' +import { defineConfig } from 'eslint/config' import globals from 'globals' import tseslint from 'typescript-eslint' +import vueParser from 'vue-eslint-parser' -export default [ - { - files: ['src/**/*.{js,mjs,cjs,ts,vue}'] - }, +const extraFileExtensions = ['.vue'] + +export default defineConfig([ { ignores: [ 'src/scripts/*', @@ -24,35 +25,55 @@ export default [ ] }, { + files: ['./**/*.{ts,mts}'], languageOptions: { globals: { ...globals.browser, __COMFYUI_FRONTEND_VERSION__: 'readonly' }, - parser: tseslint.parser, parserOptions: { - project: ['./tsconfig.json', './tsconfig.eslint.json'], + parser: tseslint.parser, + projectService: { + allowDefaultProject: [ + 'vite.config.mts', + 'vite.electron.config.mts', + 'vite.types.config.mts' + ] + }, + tsConfigRootDir: import.meta.dirname, ecmaVersion: 2020, sourceType: 'module', - extraFileExtensions: ['.vue'] + extraFileExtensions + } + } + }, + { + files: ['./**/*.vue'], + languageOptions: { + globals: { + ...globals.browser, + __COMFYUI_FRONTEND_VERSION__: 'readonly' + }, + parser: vueParser, + parserOptions: { + parser: tseslint.parser, + projectService: true, + tsConfigRootDir: import.meta.dirname, + ecmaVersion: 2020, + sourceType: 'module', + extraFileExtensions } } }, pluginJs.configs.recommended, - ...tseslint.configs.recommended, - ...pluginVue.configs['flat/recommended'], + tseslint.configs.recommended, + pluginVue.configs['flat/recommended'], eslintPluginPrettierRecommended, - { - files: ['src/**/*.vue'], - languageOptions: { - parserOptions: { - parser: tseslint.parser - } - } - }, + storybook.configs['flat/recommended'], { plugins: { 'unused-imports': unusedImports, + // @ts-expect-error Bad types in the plugin '@intlify/vue-i18n': pluginI18n }, rules: { @@ -60,10 +81,29 @@ export default [ '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/prefer-as-const': 'off', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-empty-object-type': [ + 'error', + { + allowInterfaces: 'always' + } + ], 'unused-imports/no-unused-imports': 'error', 'vue/no-v-html': 'off', // Enforce dark-theme: instead of dark: prefix 'vue/no-restricted-class': ['error', '/^dark:/'], + 'vue/multi-word-component-names': 'off', // TODO: fix + 'vue/no-template-shadow': 'off', // TODO: fix + /* Toggle on to do additional until we can clean up existing violations. + 'vue/no-unused-emit-declarations': 'error', + 'vue/no-unused-properties': 'error', + 'vue/no-unused-refs': 'error', + 'vue/no-use-v-else-with-v-for': 'error', + 'vue/no-useless-v-bind': 'error', + // */ + 'vue/one-component-per-file': 'off', // TODO: fix + 'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile // Restrict deprecated PrimeVue components 'no-restricted-imports': [ 'error', @@ -133,5 +173,13 @@ export default [ ] } }, - ...storybook.configs['flat/recommended'] -] + { + files: ['tests-ui/**/*'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false } + ] + } + } +]) diff --git a/index.html b/index.html index de7710c63..8684af476 100644 --- a/index.html +++ b/index.html @@ -8,8 +8,8 @@ - - + + diff --git a/knip.config.ts b/knip.config.ts index 9df077d77..c91a67d56 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -22,7 +22,7 @@ const config: KnipConfig = { ], ignore: [ // Auto generated manager types - 'src/types/generatedManagerTypes.ts', + 'src/workbench/extensions/manager/types/generatedManagerTypes.ts', 'src/types/comfyRegistryTypes.ts', // Used by a custom node (that should move off of this) 'src/scripts/ui/components/splitButton.ts' diff --git a/lint-staged.config.js b/lint-staged.config.js index 2d1a6f051..0f3808700 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -3,13 +3,13 @@ export default { './**/*.{ts,tsx,vue,mts}': (stagedFiles) => [ ...formatAndEslint(stagedFiles), - 'vue-tsc --noEmit' + 'pnpm typecheck' ] } function formatAndEslint(fileNames) { return [ - `eslint --fix ${fileNames.join(' ')}`, - `prettier --write ${fileNames.join(' ')}` + `pnpm exec eslint --cache --fix ${fileNames.join(' ')}`, + `pnpm exec prettier --cache --write ${fileNames.join(' ')}` ] } diff --git a/package.json b/package.json index 3ac3f0c35..6452e49c3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.27.2", + "version": "1.28.2", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -14,21 +14,24 @@ "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", "zipdist": "node scripts/zipdist.js", "typecheck": "vue-tsc --noEmit", - "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache", + "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different", "format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache", - "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'", + "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different", "format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'", + "test:all": "nx run test", "test:browser": "npx nx e2e", - "test:unit": "nx run test tests-ui/tests", "test:component": "nx run test src/components/", "test:litegraph": "vitest run --config vitest.litegraph.config.ts", + "test:unit": "nx run test tests-ui/tests", "preinstall": "npx only-allow pnpm", "prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true", "preview": "nx preview", - "lint": "eslint src --cache --concurrency=$npm_package_config_eslint_concurrency", - "lint:fix": "eslint src --fix --cache --concurrency=$npm_package_config_eslint_concurrency", - "lint:no-cache": "eslint src --concurrency=$npm_package_config_eslint_concurrency", - "lint:fix:no-cache": "eslint src --fix --concurrency=$npm_package_config_eslint_concurrency", + "lint": "eslint src --cache", + "lint:fix": "eslint src --cache --fix", + "lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache", + "lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix", + "lint:no-cache": "eslint src", + "lint:fix:no-cache": "eslint src --fix", "knip": "knip --cache", "knip:no-cache": "knip", "locale": "lobe-i18n locale", @@ -37,14 +40,11 @@ "storybook": "nx storybook -p 6006", "build-storybook": "storybook build" }, - "config": { - "eslint_concurrency": "4" - }, "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^9.35.0", "@iconify-json/lucide": "^1.2.66", "@iconify/tailwind": "^1.2.0", - "@intlify/eslint-plugin-vue-i18n": "^3.2.0", + "@intlify/eslint-plugin-vue-i18n": "^4.1.0", "@lobehub/i18n-cli": "^1.25.1", "@nx/eslint": "21.4.1", "@nx/playwright": "21.4.1", @@ -67,11 +67,11 @@ "@vitest/ui": "^3.0.0", "@vue/test-utils": "^2.4.6", "eslint": "^9.34.0", - "eslint-config-prettier": "^10.1.2", - "eslint-plugin-prettier": "^5.2.6", - "eslint-plugin-storybook": "^9.1.1", - "eslint-plugin-unused-imports": "^4.1.4", - "eslint-plugin-vue": "^9.27.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-storybook": "^9.1.6", + "eslint-plugin-unused-imports": "^4.2.0", + "eslint-plugin-vue": "^10.4.0", "fs-extra": "^11.2.0", "globals": "^15.9.0", "happy-dom": "^15.11.0", @@ -82,28 +82,32 @@ "lint-staged": "^15.2.7", "nx": "21.4.1", "prettier": "^3.3.2", - "storybook": "^9.1.1", + "storybook": "^9.1.6", "tailwindcss": "^4.1.12", "tailwindcss-primeui": "^0.6.1", "tsx": "^4.15.6", + "tw-animate-css": "^1.3.8", "typescript": "^5.4.5", - "typescript-eslint": "^8.42.0", + "typescript-eslint": "^8.44.0", "unplugin-icons": "^0.22.0", "unplugin-vue-components": "^0.28.0", "uuid": "^11.1.0", "vite": "^5.4.19", - "vite-plugin-dts": "^4.3.0", + "vite-plugin-dts": "^4.5.4", "vite-plugin-html": "^3.2.2", "vite-plugin-vue-devtools": "^7.7.6", "vitest": "^3.2.4", - "vue-tsc": "^2.1.10", + "vue-component-type-helpers": "^3.0.7", + "vue-eslint-parser": "^10.2.0", + "vue-tsc": "^3.0.7", "zip-dir": "^2.0.0", "zod-to-json-schema": "^3.24.1" }, "dependencies": { "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", - "@comfyorg/comfyui-electron-types": "^0.4.69", + "@comfyorg/comfyui-electron-types": "0.4.73-0", + "@comfyorg/tailwind-utils": "workspace:*", "@iconify/json": "^2.2.380", "@primeuix/forms": "0.0.2", "@primeuix/styled": "0.3.2", @@ -127,7 +131,6 @@ "algoliasearch": "^5.21.0", "axios": "^1.8.2", "chart.js": "^4.5.0", - "clsx": "^2.1.1", "dompurify": "^3.2.5", "dotenv": "^16.4.5", "es-toolkit": "^1.39.9", @@ -143,8 +146,8 @@ "pinia": "^2.1.7", "primeicons": "^7.0.0", "primevue": "^4.2.5", + "reka-ui": "^2.5.0", "semver": "^7.7.2", - "tailwind-merge": "^3.3.1", "three": "^0.170.0", "tiptap-markdown": "^0.8.10", "vue": "^3.5.13", diff --git a/packages/tailwind-utils/README.md b/packages/tailwind-utils/README.md new file mode 100644 index 000000000..5f315600b --- /dev/null +++ b/packages/tailwind-utils/README.md @@ -0,0 +1,31 @@ +# @comfyorg/tailwind-utils + +Shared Tailwind CSS utility functions for the ComfyUI Frontend monorepo. + +## Usage + +The `cn` function combines `clsx` and `tailwind-merge` to handle conditional classes and resolve Tailwind conflicts. + +```typescript +import { cn } from '@comfyorg/tailwind-utils' + +// Use with conditional classes (object) +
+ +// Use with conditional classes (ternary) +