diff --git a/.claude/commands/comprehensive-pr-review.md b/.claude/commands/comprehensive-pr-review.md index 84708564ea..1b4047e782 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/.gitattributes b/.gitattributes index de05efbf41..bd0518cde4 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 907695e573..178bd4ee81 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,10 +201,18 @@ jobs: if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success env: GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | + # 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}" @@ -165,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 a5617c1964..f46e9b7ac9 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/test-ui.yaml b/.github/workflows/test-ui.yaml index eaaaefee09..4f05a6d26c 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 diff --git a/.gitignore b/.gitignore index 5a58d1b1a1..e5bb5f1075 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/ @@ -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 0429c35781..2efe5f9662 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/.storybook/main.ts b/.storybook/main.ts index a799ec143e..aa6bb1fbd1 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 747bbe8029..bfe81f4311 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) - const pinia = createPinia() - app.use(pinia) - // Initialize stores - useColorPaletteStore(pinia) - useWidgetStore(pinia) + // Create Pinia instance + const pinia = createPinia() + app.use(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 8d4e4a90fe..cd1b4e5087 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 ede6a303a9..ce0ed1f36a 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 0000000000..9b665191db --- /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 c9a8820f5e..19796f4c4c 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1643,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 62a9613757..ff0735e171 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 index e3b3de5425..b517502997 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -22,6 +22,13 @@ export class VueNodeHelpers { ) } + /** + * 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 */ diff --git a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts index 23dc104cf4..fd40ca9113 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 afaf86154b..e9040a3a9a 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 7baaa1ef99..f3fbe42cfb 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 04a9117ce2..6d0cd1fb34 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/ws.ts b/browser_tests/fixtures/ws.ts index e12c534652..f1ab1a538c 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 12033fce34..881ef11c43 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 47bab3db97..aeed77294c 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 0000000000..af6c10e9d3 --- /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 a444a97c6a..45010b979f 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 0d2c9f31ea..c690b8702a 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -1,7 +1,7 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import path from 'path' -import { +import type { TemplateInfo, WorkflowTemplates } from '../../src/platform/workflow/templates/types/template' diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index a504ea4fc1..b23e4466d0 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 24af9e8acd..7f3ed6a3d4 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 7a32833e4f..8e39154f15 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 db33975143..c47a4d19b0 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 901cce9137..6dd53c194f 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 4225ad228c..e271f2e15c 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 3bcee65f0e..cabb849e80 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 cf2e5e6be0..7459acf585 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 91d53c4078..6517b9170d 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 90bc677fcf..2b89be5c53 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 4adab98b60..075025a3ab 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 09a08384c8..38f4a6c1de 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/platform/settings/types' +import type { SettingParams } from '../../src/platform/settings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Topbar commands', () => { diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 73eb35f472..38286b3990 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 25e166bab8..cd89e92d5f 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 9ae090975b..daa165fa47 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 f5ca69e541..2736a50c57 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 af9081d0a9..fd72c2d0a7 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 41b50224ad..9a23102312 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 c3754ed3bd..2fc7534909 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 }) => { diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts index ced2936378..f4244ae669 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 8d8f6c2e85..184943fe05 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 678cb60f07..f091058d24 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 025347e4de..154ac3c16f 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 984dd6ea1f..111efe29cf 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 2b76d45427..fdaae14bcb 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/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index 3c5e3cbe24..98ba335836 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 0f3d6a3178..52dc575423 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 7fc408e8b8..0584a3bec2 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 05bb578df9..7a54cae07b 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'] diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 89fdf38b29..0b2b1e0f62 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 6e63295b85..a9e9926bfa 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 db21ecd360..f7718122b7 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/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index a9a5fc9c20..6b85769826 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/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts index a7311c15a3..db63261528 100644 --- a/browser_tests/tests/selectionToolboxSubmenus.spec.ts +++ b/browser_tests/tests/selectionToolboxSubmenus.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('Selection Toolbox - More Options Submenus', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) diff --git a/browser_tests/tests/sidebar/queue.spec.ts b/browser_tests/tests/sidebar/queue.spec.ts index 2d9dd10bae..39e2ced6e1 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 f2c2e2bb5d..9141e9135b 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/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index 69a40ced92..a817616f8b 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 d85f187238..eee6fda925 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 7a8ae5dd2b..336e2672de 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 index a00d93eb04..51b52e7ce3 100644 --- a/browser_tests/tests/vueNodes/deleteKeyInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/deleteKeyInteraction.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('Vue Nodes - Delete Key Interaction', () => { test.beforeEach(async ({ comfyPage }) => { // Enable Vue nodes rendering diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/linkInteraction.spec.ts new file mode 100644 index 0000000000..d6b1bccc18 --- /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 0000000000..78e331b3ca 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 0000000000..2ed598ef88 --- /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 0000000000..744e405944 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 0000000000..5dfa61c196 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 0000000000..8091e5d036 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 0000000000..591c1d307b --- /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 0000000000..74ec17cc90 --- /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 0000000000..f4f8e10fe0 --- /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 0000000000..37dcfd37b5 --- /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 728b5d0285..3b9c057847 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) @@ -318,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/tsconfig.json b/browser_tests/tsconfig.json new file mode 100644 index 0000000000..391298333b --- /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 3f795a2194..5b7f4bec42 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 80ccb6c9f7..bbbf14c2c6 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 0000000000..1c24810a8e --- /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/eslint.config.ts b/eslint.config.ts index 191e07ba96..ab3bf09f54 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -33,7 +33,13 @@ export default defineConfig([ }, parserOptions: { parser: tseslint.parser, - projectService: true, + projectService: { + allowDefaultProject: [ + 'vite.config.mts', + 'vite.electron.config.mts', + 'vite.types.config.mts' + ] + }, tsConfigRootDir: import.meta.dirname, ecmaVersion: 2020, sourceType: 'module', @@ -75,12 +81,27 @@ export default defineConfig([ '@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 @@ -151,5 +172,14 @@ export default defineConfig([ } ] } + }, + { + files: ['tests-ui/**/*'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false } + ] + } } ]) diff --git a/index.html b/index.html index de7710c63d..8684af476b 100644 --- a/index.html +++ b/index.html @@ -8,8 +8,8 @@ - - + + diff --git a/knip.config.ts b/knip.config.ts index 81911a7361..0dcbf7d500 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/package.json b/package.json index 9acc66aa66..b3f3cb8b6b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.27.5", + "version": "1.27.6", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -18,15 +18,18 @@ "format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache", "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", "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", @@ -94,6 +97,7 @@ "vite-plugin-html": "^3.2.2", "vite-plugin-vue-devtools": "^7.7.6", "vitest": "^3.2.4", + "vue-component-type-helpers": "^3.0.7", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.0.7", "zip-dir": "^2.0.0", @@ -103,6 +107,7 @@ "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@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", @@ -126,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", @@ -144,7 +148,6 @@ "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 0000000000..5f315600b2 --- /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) +