diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index f46e9b7ac9..959d017395 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -78,7 +78,7 @@ jobs: wait-for-it --service 127.0.0.1:8188 -t 600 working-directory: ComfyUI - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Start dev server # Run electron dev server as it is a superset of the web dev server @@ -86,7 +86,7 @@ jobs: run: pnpm dev:electron & working-directory: ComfyUI_frontend - name: Capture base i18n - run: npx tsx scripts/diff-i18n capture + run: pnpm exec tsx scripts/diff-i18n capture working-directory: ComfyUI_frontend - name: Update en.json run: pnpm collect-i18n @@ -99,7 +99,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} working-directory: ComfyUI_frontend - name: Diff base vs updated i18n - run: npx tsx scripts/diff-i18n diff + run: pnpm exec tsx scripts/diff-i18n diff working-directory: ComfyUI_frontend - name: Update i18n in custom node repository run: | diff --git a/.github/workflows/i18n-node-defs.yaml b/.github/workflows/i18n-node-defs.yaml index 1327db3cf7..d9105a4ac2 100644 --- a/.github/workflows/i18n-node-defs.yaml +++ b/.github/workflows/i18n-node-defs.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3 - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Start dev server # Run electron dev server as it is a superset of the web dev server diff --git a/.github/workflows/i18n.yaml b/.github/workflows/i18n.yaml index d7df815ff6..566a335b50 100644 --- a/.github/workflows/i18n.yaml +++ b/.github/workflows/i18n.yaml @@ -33,7 +33,7 @@ jobs: restore-keys: | playwright-browsers-${{ runner.os }}- - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Start dev server # Run electron dev server as it is a superset of the web dev server diff --git a/.github/workflows/test-browser-exp.yaml b/.github/workflows/test-browser-exp.yaml index 63052c3e46..e174e89c33 100644 --- a/.github/workflows/test-browser-exp.yaml +++ b/.github/workflows/test-browser-exp.yaml @@ -19,11 +19,11 @@ jobs: restore-keys: | playwright-browsers-${{ runner.os }}- - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: ComfyUI_frontend - name: Run Playwright tests and update snapshots id: playwright-tests - run: npx playwright test --update-snapshots + run: pnpm exec playwright test --update-snapshots continue-on-error: true working-directory: ComfyUI_frontend - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index 4f05a6d26c..640615d99e 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -148,7 +148,7 @@ jobs: - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) id: playwright - run: npx playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob + run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob working-directory: ComfyUI_frontend env: PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report @@ -230,7 +230,7 @@ jobs: run: | # Run tests with both HTML and JSON reporters PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ - npx playwright test --project=${{ matrix.browser }} \ + pnpm exec playwright test --project=${{ matrix.browser }} \ --reporter=list \ --reporter=html \ --reporter=json @@ -281,10 +281,10 @@ jobs: - name: Merge into HTML Report run: | # Generate HTML report - npx playwright merge-reports --reporter=html ./all-blob-reports + pnpm exec playwright merge-reports --reporter=html ./all-blob-reports # Generate JSON report separately with explicit output path PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ - npx playwright merge-reports --reporter=json ./all-blob-reports + pnpm exec playwright merge-reports --reporter=json ./all-blob-reports working-directory: ComfyUI_frontend - name: Upload HTML report diff --git a/.github/workflows/update-manager-types.yaml b/.github/workflows/update-manager-types.yaml index 244127dc2a..de5b799da5 100644 --- a/.github/workflows/update-manager-types.yaml +++ b/.github/workflows/update-manager-types.yaml @@ -68,7 +68,7 @@ jobs: - name: Generate Manager API types run: | echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..." - npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts + pnpm dlx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts - name: Validate generated types run: | diff --git a/.github/workflows/update-registry-types.yaml b/.github/workflows/update-registry-types.yaml index 0cd2c41dae..4a84e9f432 100644 --- a/.github/workflows/update-registry-types.yaml +++ b/.github/workflows/update-registry-types.yaml @@ -68,7 +68,7 @@ jobs: - name: Generate API types run: | echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..." - npx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts + pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts - name: Validate generated types run: | diff --git a/.gitignore b/.gitignore index 32e1b6624c..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 diff --git a/.husky/pre-commit b/.husky/pre-commit index 5782715092..c0b5cf4376 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env bash pnpm exec lint-staged -pnpm exec tsx scripts/check-unused-i18n-keys.ts +pnpm exec tsx scripts/check-unused-i18n-keys.ts \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index aa6bb1fbd1..e8021974b9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -45,7 +45,7 @@ const config: StorybookConfig = { compiler: 'vue3', customCollections: { comfy: FileSystemIconLoader( - process.cwd() + '/src/assets/icons/custom' + process.cwd() + '/packages/design-system/src/icons' ) } }), diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f4fd8db8d..6614fe619d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -265,9 +265,9 @@ The project supports three types of icons, all with automatic imports (no manual 2. **Iconify Icons** - 200,000+ icons from various libraries: ``, `` 3. **Custom Icons** - Your own SVG icons: `` -Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation. +Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation. -For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md). +For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md). ## Working with litegraph.js diff --git a/browser_tests/README.md b/browser_tests/README.md index 021c063ae8..4954ba9fd0 100644 --- a/browser_tests/README.md +++ b/browser_tests/README.md @@ -29,7 +29,7 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/ Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver: ```bash -npx playwright install chromium --with-deps +pnpm exec playwright install chromium --with-deps ``` ### Environment Configuration @@ -56,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. @@ -81,7 +73,7 @@ For tests that specifically need to test release functionality, see the example **Always use UI mode for development:** ```bash -npx playwright test --ui +pnpm exec playwright test --ui ``` UI mode features: @@ -97,8 +89,8 @@ UI mode features: For CI or headless testing: ```bash -npx playwright test # Run all tests -npx playwright test widget.spec.ts # Run specific test file +pnpm exec playwright test # Run all tests +pnpm exec playwright test widget.spec.ts # Run specific test file ``` ### Local Development Config @@ -394,7 +386,7 @@ export default defineConfig({ Option 2 - Generate local baselines for comparison: ```bash -npx playwright test --update-snapshots +pnpm exec playwright test --update-snapshots ``` ### Creating New Screenshot Baselines diff --git a/browser_tests/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/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/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 8c23c835a8..8e39154f15 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -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 7d1bf6c105..c47a4d19b0 100644 --- a/browser_tests/tests/chatHistory.spec.ts +++ b/browser_tests/tests/chatHistory.spec.ts @@ -3,6 +3,10 @@ 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 c86466215f..7459acf585 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -4,6 +4,10 @@ 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/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/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 fc8dbd646a..9a23102312 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -4,6 +4,10 @@ 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 bd14f91ada..2fc7534909 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -9,6 +9,10 @@ import { } from '../fixtures/ComfyPage' 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 }) => { await comfyPage.loadWorkflow('groups/mixed_graph_items') 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/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/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/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 9141e9135b..6252332135 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -80,6 +80,12 @@ test.describe('Templates', () => { // Load a template await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() + + await comfyPage.page + .locator( + 'nav > div:nth-child(2) > div > span:has-text("Getting Started")' + ) + .click() await comfyPage.templates.loadTemplate('default') await expect(comfyPage.templates.content).toBeHidden() @@ -102,48 +108,72 @@ test.describe('Templates', () => { expect(await comfyPage.templates.content.isVisible()).toBe(true) }) - test('Uses title field as fallback when the key is not found in locales', async ({ + test('Uses proper locale files for templates', async ({ comfyPage }) => { + // Set locale to French before opening templates + await comfyPage.setSetting('Comfy.Locale', 'fr') + + // Load the templates dialog and wait for the French index file request + const requestPromise = comfyPage.page.waitForRequest( + '**/templates/index.fr.json' + ) + + await comfyPage.executeCommand('Comfy.BrowseTemplates') + + const request = await requestPromise + + // Verify French index was requested + expect(request.url()).toContain('templates/index.fr.json') + + await expect(comfyPage.templates.content).toBeVisible() + }) + + test('Falls back to English templates when locale file not found', async ({ comfyPage }) => { - // Capture request for the index.json - await comfyPage.page.route('**/templates/index.json', async (route, _) => { - // Add a new template that won't have a translation pre-generated - const response = [ - { - moduleName: 'default', - title: 'FALLBACK CATEGORY', - type: 'image', - templates: [ - { - name: 'unknown_key_has_no_translation_available', - title: 'FALLBACK TEMPLATE NAME', - mediaType: 'image', - mediaSubtype: 'webp', - description: 'No translations found' - } - ] - } - ] + // Set locale to a language that doesn't have a template file + await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists + + // Wait for the German request (expected to 404) + const germanRequestPromise = comfyPage.page.waitForRequest( + '**/templates/index.de.json' + ) + + // Wait for the fallback English request + const englishRequestPromise = comfyPage.page.waitForRequest( + '**/templates/index.json' + ) + + // Intercept the German file to simulate a 404 + await comfyPage.page.route('**/templates/index.de.json', async (route) => { await route.fulfill({ - status: 200, - body: JSON.stringify(response), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } + status: 404, + headers: { 'Content-Type': 'text/plain' }, + body: 'Not Found' }) }) + // Allow the English index to load normally + await comfyPage.page.route('**/templates/index.json', (route) => + route.continue() + ) + // Load the templates dialog await comfyPage.executeCommand('Comfy.BrowseTemplates') + await expect(comfyPage.templates.content).toBeVisible() - // Expect the title to be used as fallback for template cards + // Verify German was requested first, then English as fallback + const germanRequest = await germanRequestPromise + const englishRequest = await englishRequestPromise + + expect(germanRequest.url()).toContain('templates/index.de.json') + expect(englishRequest.url()).toContain('templates/index.json') + + // Verify English titles are shown as fallback await expect( - comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME') + comfyPage.templates.content.getByRole('heading', { + name: 'Image Generation' + }) ).toBeVisible() - - // Expect the title to be used as fallback for the template categories - await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible() }) test('template cards are dynamically sized and responsive', async ({ @@ -151,46 +181,33 @@ test.describe('Templates', () => { }) => { // Open templates dialog await comfyPage.executeCommand('Comfy.BrowseTemplates') - await expect(comfyPage.templates.content).toBeVisible() + await comfyPage.templates.content.waitFor({ state: 'visible' }) - // Wait for at least one template card to appear - await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({ - timeout: 5000 - }) + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) + const nav = comfyPage.page + .locator('header') + .filter({ hasText: 'Templates' }) - // Take snapshot of the template grid - const templateGrid = comfyPage.templates.content.locator('.grid').first() + const cardCount = await comfyPage.page + .locator('[data-testid^="template-workflow-"]') + .count() + expect(cardCount).toBeGreaterThan(0) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png') + await expect(nav).toBeVisible() // Nav should be visible at desktop size - // Check cards at mobile viewport size - await comfyPage.page.setViewportSize({ width: 640, height: 800 }) + const mobileSize = { width: 640, height: 800 } + await comfyPage.page.setViewportSize(mobileSize) + expect(cardCount).toBeGreaterThan(0) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png') + await expect(nav).not.toBeVisible() // Nav should collapse at mobile size - // Check cards at tablet size - await comfyPage.page.setViewportSize({ width: 1024, height: 800 }) + const tabletSize = { width: 1024, height: 800 } + await comfyPage.page.setViewportSize(tabletSize) + expect(cardCount).toBeGreaterThan(0) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png') - }) - - test('hover effects work on template cards', async ({ comfyPage }) => { - // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') - await expect(comfyPage.templates.content).toBeVisible() - - // Get a template card - const firstCard = comfyPage.page.locator('.template-card').first() - await expect(firstCard).toBeVisible({ timeout: 5000 }) - - // Take snapshot before hover - await expect(firstCard).toHaveScreenshot('template-card-before-hover.png') - - // Hover over the card - await firstCard.hover() - - // Take snapshot after hover to verify hover effect - await expect(firstCard).toHaveScreenshot('template-card-after-hover.png') + await expect(nav).toBeVisible() // Nav should be visible at tablet size }) test('template cards descriptions adjust height dynamically', async ({ @@ -257,21 +274,42 @@ test.describe('Templates', () => { await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() - // Verify cards are visible with varying content lengths + // Wait for cards to load await expect( - comfyPage.page.getByText('This is a short description.') - ).toBeVisible({ timeout: 5000 }) - await expect( - comfyPage.page.getByText('This is a medium length description') - ).toBeVisible({ timeout: 5000 }) - await expect( - comfyPage.page.getByText('This is a much longer description') + comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) ).toBeVisible({ timeout: 5000 }) - // Take snapshot of a grid with specific cards - const templateGrid = comfyPage.templates.content - .locator('.grid:has-text("Short Description")') - .first() + // Verify all three cards with different descriptions are visible + const shortDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) + const mediumDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-medium-description"]' + ) + const longDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-long-description"]' + ) + + await expect(shortDescCard).toBeVisible() + await expect(mediumDescCard).toBeVisible() + await expect(longDescCard).toBeVisible() + + // Verify descriptions are visible and have line-clamp class + // The description is in a p tag with text-muted class + const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2') + const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2') + const longDesc = longDescCard.locator('p.text-muted.line-clamp-2') + + await expect(shortDesc).toContainText('short description') + await expect(mediumDesc).toContainText('medium length description') + await expect(longDesc).toContainText('much longer description') + + // Verify grid layout maintains consistency + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) await expect(templateGrid).toBeVisible() await expect(templateGrid).toHaveScreenshot( 'template-grid-varying-content.png' diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png index ce88325aad..137cb4d8c4 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 bf1d18934d..080855e15c 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png index 6c330c2f46..2548e66ae8 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index 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 b2c62aeb03..4b3ff3e309 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -85,6 +85,7 @@ test.describe('Version Mismatch Warnings', () => { test('should persist dismissed state across sessions', async ({ comfyPage }) => { + test.setTimeout(30_000) // Mock system_stats route to indicate that the installed version is always ahead of the required version await comfyPage.page.route('**/system_stats**', async (route) => { await route.fulfill({ @@ -106,6 +107,11 @@ test.describe('Version Mismatch Warnings', () => { const dismissButton = warningToast.getByRole('button', { name: 'Close' }) await dismissButton.click() + // Wait for the dismissed state to be persisted + await comfyPage.page.waitForFunction( + () => !!localStorage.getItem('comfy.versionMismatch.dismissals') + ) + // Reload the page, keeping local storage await comfyPage.setup({ clearStorage: false }) diff --git a/browser_tests/tests/vueNodes/NodeHeader.spec.ts b/browser_tests/tests/vueNodes/NodeHeader.spec.ts index 336e2672de..83e6b8aae8 100644 --- a/browser_tests/tests/vueNodes/NodeHeader.spec.ts +++ b/browser_tests/tests/vueNodes/NodeHeader.spec.ts @@ -61,6 +61,19 @@ test.describe('NodeHeader', () => { expect(titleAfterCancel).toBe('My Custom Sampler') }) + test('Double click node body does not trigger edit', async ({ + comfyPage + }) => { + const loadCheckpointNode = + comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const nodeBbox = await loadCheckpointNode.boundingBox() + if (!nodeBbox) throw new Error('Node not found') + await loadCheckpointNode.dblclick() + + const editingTitleInput = comfyPage.page.getByTestId('node-title-input') + await expect(editingTitleInput).not.toBeVisible() + }) + test('handles node collapsing', async ({ comfyPage }) => { // Get the KSampler node from the default workflow const nodes = await comfyPage.getNodeRefsByType('KSampler') 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/interactions/zoom.spec.ts b/browser_tests/tests/vueNodes/interactions/zoom.spec.ts new file mode 100644 index 0000000000..b28caec406 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/zoom.spec.ts @@ -0,0 +1,33 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.describe('Vue Nodes Zoom', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should not capture drag while zooming with ctrl+shift+drag', async ({ + comfyPage + }) => { + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const nodeBoundingBox = await checkpointNode.boundingBox() + if (!nodeBoundingBox) throw new Error('Node bounding box not available') + + const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2 + const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2 + + // Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over + // the node. The node should not capture the drag while drag-zooming. + await comfyPage.page.keyboard.down('Control') + await comfyPage.page.keyboard.down('Shift') + await comfyPage.dragAndDrop( + { x: 200, y: 300 }, + { x: nodeMidpointX, y: nodeMidpointY } + ) + + await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png') + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png new file mode 100644 index 0000000000..1f47aab5c5 Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/lod.spec.ts b/browser_tests/tests/vueNodes/lod.spec.ts index 9011f91b10..2ed598ef88 100644 --- a/browser_tests/tests/vueNodes/lod.spec.ts +++ b/browser_tests/tests/vueNodes/lod.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 - LOD', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) 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 index 8e93b88f3f..c9ab0ed1af 100644 Binary files a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png 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-inactive-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png index 59802088fc..55b9bce6f1 100644 Binary files a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png 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 index ff8b6f9518..591c1d307b 100644 --- a/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts +++ b/browser_tests/tests/vueNodes/nodeInteractions/selectionState.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('Vue Node Selection', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts index c80a865031..74ec17cc90 100644 --- a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -15,11 +15,10 @@ test.describe('Vue Node Bypass', () => { test('should allow toggling bypass on a selected node with hotkey', async ({ comfyPage }) => { - const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({ - hasText: 'Load Checkpoint' - }) - await checkpointNode.getByText('Load Checkpoint').click() + 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) @@ -29,15 +28,12 @@ test.describe('Vue Node Bypass', () => { test('should allow toggling bypass on multiple selected nodes with hotkey', async ({ comfyPage }) => { - const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({ - hasText: 'Load Checkpoint' - }) - const ksamplerNode = comfyPage.page.locator('[data-node-id]').filter({ - hasText: 'KSampler' - }) + 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 checkpointNode.getByText('Load Checkpoint').click() - await ksamplerNode.getByText('KSampler').click({ modifiers: ['Control'] }) await comfyPage.page.keyboard.press(BYPASS_HOTKEY) await expect(checkpointNode).toHaveClass(BYPASS_CLASS) await expect(ksamplerNode).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/vueNodes/nodeStates/pin.spec.ts b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts new file mode 100644 index 0000000000..27f1ad1ac9 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts @@ -0,0 +1,85 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const PIN_HOTKEY = 'p' +const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' + +test.describe('Vue Node Pin', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling pin on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const pinIndicator = checkpointNode.locator(PIN_INDICATOR) + + await expect(pinIndicator).toBeVisible() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + await expect(pinIndicator).not.toBeVisible() + }) + + test('should allow toggling pin on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + const pinIndicator1 = checkpointNode.locator(PIN_INDICATOR) + await expect(pinIndicator1).toBeVisible() + const pinIndicator2 = ksamplerNode.locator(PIN_INDICATOR) + await expect(pinIndicator2).toBeVisible() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + await expect(pinIndicator1).not.toBeVisible() + await expect(pinIndicator2).not.toBeVisible() + }) + + test('should not allow dragging pinned nodes', async ({ comfyPage }) => { + const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint') + await checkpointNodeHeader.click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + // Try to drag the node + const headerPos = await checkpointNodeHeader.boundingBox() + if (!headerPos) throw new Error('Failed to get header position') + await comfyPage.dragAndDrop( + { x: headerPos.x, y: headerPos.y }, + { x: headerPos.x + 256, y: headerPos.y + 256 } + ) + + // Verify the node is not dragged (same position before and after click-and-drag) + const headerPosAfterDrag = await checkpointNodeHeader.boundingBox() + if (!headerPosAfterDrag) + throw new Error('Failed to get header position after drag') + expect(headerPosAfterDrag).toEqual(headerPos) + + // Unpin the node with the hotkey + await checkpointNodeHeader.click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + // Try to drag the node again + await comfyPage.dragAndDrop( + { x: headerPos.x, y: headerPos.y }, + { x: headerPos.x + 256, y: headerPos.y + 256 } + ) + + // Verify the node is dragged + const headerPosAfterDrag2 = await checkpointNodeHeader.boundingBox() + if (!headerPosAfterDrag2) + throw new Error('Failed to get header position after drag') + expect(headerPosAfterDrag2).not.toEqual(headerPos) + }) +}) diff --git a/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts new file mode 100644 index 0000000000..bb08232a29 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts @@ -0,0 +1,49 @@ +import { + type ComfyPage, + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.describe('Vue Multiline String Widget', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + const getFirstClipNode = (comfyPage: ComfyPage) => + comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode (Prompt)').first() + + const getFirstMultilineStringWidget = (comfyPage: ComfyPage) => + getFirstClipNode(comfyPage).getByRole('textbox', { name: 'text' }) + + test('should allow entering text', async ({ comfyPage }) => { + const textarea = getFirstMultilineStringWidget(comfyPage) + await textarea.fill('Hello World') + await expect(textarea).toHaveValue('Hello World') + await textarea.fill('Hello World 2') + await expect(textarea).toHaveValue('Hello World 2') + }) + + test('should support entering multiline content', async ({ comfyPage }) => { + const textarea = getFirstMultilineStringWidget(comfyPage) + + const multilineValue = ['Line 1', 'Line 2', 'Line 3'].join('\n') + + await textarea.fill(multilineValue) + await expect(textarea).toHaveValue(multilineValue) + }) + + test('should retain value after focus changes', async ({ comfyPage }) => { + const textarea = getFirstMultilineStringWidget(comfyPage) + + await textarea.fill('Keep me around') + + // Click another node + const loadCheckpointNode = + comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + await loadCheckpointNode.click() + await getFirstClipNode(comfyPage).click() + + await expect(textarea).toHaveValue('Keep me around') + }) +}) diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index 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/docs/extensions/development.md b/docs/extensions/development.md index 47c83ecf0e..e988d1d453 100644 --- a/docs/extensions/development.md +++ b/docs/extensions/development.md @@ -110,7 +110,7 @@ pnpm build For faster iteration during development, use watch mode: ```bash -npx vite build --watch +pnpm exec vite build --watch ``` Note: Watch mode provides faster rebuilds than full builds, but still no hot reload diff --git a/eslint.config.ts b/eslint.config.ts index ab3bf09f54..22251afdb8 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -181,5 +181,31 @@ export default defineConfig([ { disallowTypeAnnotations: false } ] } + }, + { + files: ['**/*.spec.ts'], + ignores: ['browser_tests/tests/**/*.spec.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: '.spec.ts files are only allowed under browser_tests/tests/' + } + ] + } + }, + { + files: ['browser_tests/tests/**/*.test.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: + '.test.ts files are not allowed in browser_tests/tests/; use .spec.ts instead' + } + ] + } } ]) diff --git a/knip.config.ts b/knip.config.ts index 0dcbf7d500..3022a0af54 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,15 +1,25 @@ import type { KnipConfig } from 'knip' const config: KnipConfig = { - entry: [ - '{build,scripts}/**/*.{js,ts}', - 'src/assets/css/style.css', - 'src/main.ts', - 'src/scripts/ui/menu/index.ts', - 'src/types/index.ts' - ], - project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'], - ignoreBinaries: ['only-allow', 'openapi-typescript'], + workspaces: { + '.': { + entry: [ + '{build,scripts}/**/*.{js,ts}', + 'src/assets/css/style.css', + 'src/main.ts', + 'src/scripts/ui/menu/index.ts', + 'src/types/index.ts' + ], + project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'] + }, + 'packages/tailwind-utils': { + project: ['src/**/*.{js,ts}'] + }, + 'packages/design-system': { + entry: ['src/**/*.ts'], + project: ['src/**/*.{js,ts}', '*.{js,ts,mts}'] + } + }, ignoreDependencies: [ // Weird importmap things '@iconify/json', @@ -25,9 +35,7 @@ const config: KnipConfig = { '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', - // Staged for for use with subgraph widget promotion - 'src/lib/litegraph/src/widgets/DisconnectedWidget.ts' + 'src/scripts/ui/components/splitButton.ts' ], compilers: { // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 diff --git a/package.json b/package.json index ec94c881bb..75a7349abe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.28.1", + "version": "1.28.2", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -18,11 +18,12 @@ "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:browser": "npx nx e2e", - "test:unit": "nx run test tests-ui/tests", + "test:all": "nx run test", + "test:browser": "pnpm exec nx e2e", "test:component": "nx run test src/components/", "test:litegraph": "vitest run --config vitest.litegraph.config.ts", - "preinstall": "npx only-allow pnpm", + "test:unit": "nx run test tests-ui/tests", + "preinstall": "pnpm dlx only-allow pnpm", "prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true", "preview": "nx preview", "lint": "eslint src --cache", @@ -34,15 +35,13 @@ "knip": "knip --cache", "knip:no-cache": "knip", "locale": "lobe-i18n locale", - "collect-i18n": "npx playwright test --config=playwright.i18n.config.ts", + "collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts", "json-schema": "tsx scripts/generate-json-schema.ts", "storybook": "nx storybook -p 6006", "build-storybook": "storybook build" }, "devDependencies": { "@eslint/js": "^9.35.0", - "@iconify-json/lucide": "^1.2.66", - "@iconify/tailwind": "^1.2.0", "@intlify/eslint-plugin-vue-i18n": "^4.1.0", "@lobehub/i18n-cli": "^1.25.1", "@nx/eslint": "21.4.1", @@ -106,6 +105,8 @@ "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "0.4.73-0", + "@comfyorg/design-system": "workspace:*", + "@comfyorg/tailwind-utils": "workspace:*", "@iconify/json": "^2.2.380", "@primeuix/forms": "0.0.2", "@primeuix/styled": "0.3.2", @@ -123,13 +124,13 @@ "@tiptap/extension-table-row": "^2.10.4", "@tiptap/starter-kit": "^2.10.4", "@vueuse/core": "^11.0.0", + "@vueuse/integrations": "^13.9.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-serialize": "^0.13.0", "@xterm/xterm": "^5.5.0", "algoliasearch": "^5.21.0", "axios": "^1.8.2", "chart.js": "^4.5.0", - "clsx": "^2.1.1", "dompurify": "^3.2.5", "dotenv": "^16.4.5", "es-toolkit": "^1.39.9", @@ -147,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", @@ -158,4 +158,4 @@ "zod": "^3.23.8", "zod-validation-error": "^3.3.0" } -} +} \ No newline at end of file diff --git a/packages/design-system/package.json b/packages/design-system/package.json new file mode 100644 index 0000000000..e2868d054e --- /dev/null +++ b/packages/design-system/package.json @@ -0,0 +1,31 @@ +{ + "name": "@comfyorg/design-system", + "version": "1.0.0", + "description": "Shared design system for ComfyUI Frontend", + "type": "module", + "exports": { + "./tailwind-config": { + "import": "./tailwind.config.ts", + "types": "./tailwind.config.ts" + }, + "./css/*": "./src/css/*" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "nx": { + "tags": [ + "scope:shared", + "type:design" + ] + }, + "dependencies": { + "@iconify-json/lucide": "^1.1.178", + "@iconify/tailwind": "^1.1.3" + }, + "devDependencies": { + "tailwindcss": "^3.4.17", + "typescript": "^5.4.5" + }, + "packageManager": "pnpm@10.17.1" +} diff --git a/src/assets/css/fonts.css b/packages/design-system/src/css/fonts.css similarity index 100% rename from src/assets/css/fonts.css rename to packages/design-system/src/css/fonts.css diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css new file mode 100644 index 0000000000..0f2bca812e --- /dev/null +++ b/packages/design-system/src/css/style.css @@ -0,0 +1,1003 @@ +@layer theme, base, primevue, components, utilities; + +@import './fonts.css'; +@import 'tailwindcss/theme' layer(theme); +@import 'tailwindcss/utilities' layer(utilities); +@import 'tw-animate-css'; + +@plugin 'tailwindcss-primeui'; + +@config '../../tailwind.config.ts'; + +:root { + --fg-color: #000; + --bg-color: #fff; + --comfy-menu-bg: #353535; + --comfy-menu-secondary-bg: #292929; + --comfy-topbar-height: 2.5rem; + --comfy-input-bg: #222; + --input-text: #ddd; + --descrip-text: #999; + --drag-text: #ccc; + --error-text: #ff4444; + --border-color: #4e4e4e; + --tr-even-bg-color: #222; + --tr-odd-bg-color: #353535; + --primary-bg: #236692; + --primary-fg: #ffffff; + --primary-hover-bg: #3485bb; + --primary-hover-fg: #ffffff; + --content-bg: #e0e0e0; + --content-fg: #000; + --content-hover-bg: #adadad; + --content-hover-fg: #000; + + /* Code styling colors for help menu*/ + --code-text-color: rgba(0, 122, 255, 1); + --code-bg-color: rgba(96, 165, 250, 0.2); + --code-block-bg-color: rgba(60, 60, 60, 0.12); +} + +@media (prefers-color-scheme: dark) { + :root { + --fg-color: #fff; + --bg-color: #202020; + --content-bg: #4e4e4e; + --content-fg: #fff; + --content-hover-bg: #222; + --content-hover-fg: #fff; + } +} + +@theme { + --text-xxs: 0.625rem; + --text-xxs--line-height: calc(1 / 0.625); + + /* Font Families */ + --font-inter: 'Inter', sans-serif; + + /* Palette Colors */ + --color-charcoal-100: #55565e; + --color-charcoal-200: #494a50; + --color-charcoal-300: #3c3d42; + --color-charcoal-400: #313235; + --color-charcoal-500: #2d2e32; + --color-charcoal-600: #262729; + --color-charcoal-700: #202121; + --color-charcoal-800: #171718; + + --color-neutral-550: #636363; + + --color-stone-100: #444444; + --color-stone-200: #828282; + --color-stone-300: #bbbbbb; + + --color-ivory-100: #fdfbfa; + --color-ivory-200: #faf9f5; + --color-ivory-300: #f0eee6; + + --color-gray-100: #f3f3f3; + --color-gray-200: #e9e9e9; + --color-gray-300: #e1e1e1; + --color-gray-400: #d9d9d9; + --color-gray-500: #c5c5c5; + --color-gray-600: #b4b4b4; + --color-gray-700: #a0a0a0; + --color-gray-800: #8a8a8a; + + --color-sand-100: #e1ded5; + --color-sand-200: #d6cfc2; + --color-sand-300: #888682; + + --color-pure-white: #ffffff; + + --color-slate-100: #9c9eab; + --color-slate-200: #9fa2bd; + --color-slate-300: #5b5e7d; + + --color-brand-yellow: #f0ff41; + --color-brand-blue: #172dd7; + + --color-blue-100: #0b8ce9; + --color-blue-200: #31b9f4; + --color-success-100: #00cd72; + --color-success-200: #47e469; + --color-warning-100: #fd9903; + --color-warning-200: #fcbf64; + --color-danger-100: #c02323; + --color-danger-200: #d62952; + + --color-coral-red-600: #973a40; + --color-coral-red-500: #c53f49; + --color-coral-red-400: #dd424e; + + --color-bypass: #6a246a; + --color-error: #962a2a; + + --color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3); + --color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15); + --color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1); + --color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4); + + /* PrimeVue pulled colors */ + --color-muted: var(--p-text-muted-color); + --color-highlight: var(--p-primary-color); + + /* Special Colors (temporary) */ + --color-dark-elevation-1.5: rgba(from white r g b/ 0.015); + --color-dark-elevation-2: rgba(from white r g b / 0.03); +} + +@theme inline { + --color-node-component-surface: var(--color-charcoal-600); + --color-node-component-surface-highlight: var(--color-slate-100); + --color-node-component-surface-hovered: var(--color-charcoal-400); + --color-node-component-surface-selected: var(--color-charcoal-200); + --color-node-stroke: var(--color-stone-100); +} + +@custom-variant dark-theme { + .dark-theme & { + @slot; + } +} + +@utility scrollbar-hide { + scrollbar-width: none; + &::-webkit-scrollbar { + width: 1px; + } + &::-webkit-scrollbar-thumb { + background-color: transparent; + } +} + +/* Everthing below here to be cleaned up over time. */ + +body { + width: 100vw; + height: 100vh; + margin: 0; + overflow: hidden; + background: var(--bg-color) var(--bg-img); + color: var(--fg-color); + min-height: -webkit-fill-available; + max-height: -webkit-fill-available; + min-width: -webkit-fill-available; + max-width: -webkit-fill-available; + font-family: Arial, sans-serif; +} + +.comfy-multiline-input { + background-color: var(--comfy-input-bg); + color: var(--input-text); + overflow: hidden; + overflow-y: auto; + padding: 2px; + resize: none; + border: none; + box-sizing: border-box; + font-size: var(--comfy-textarea-font-size); +} + +.comfy-markdown { + /* We assign the textarea and the Tiptap editor to the same CSS grid area to stack them on top of one another. */ + display: grid; +} + +.comfy-markdown > textarea { + grid-area: 1 / 1 / 2 / 2; +} + +.comfy-markdown .tiptap { + grid-area: 1 / 1 / 2 / 2; + background-color: var(--comfy-input-bg); + color: var(--input-text); + overflow: hidden; + overflow-y: auto; + resize: none; + border: none; + box-sizing: border-box; + font-size: var(--comfy-textarea-font-size); + height: 100%; + padding: 0.5em; +} + +.comfy-markdown.editing .tiptap { + display: none; +} + +.comfy-markdown .tiptap :first-child { + margin-top: 0; +} + +.comfy-markdown .tiptap :last-child { + margin-bottom: 0; +} + +.comfy-markdown .tiptap blockquote { + border-left: medium solid; + margin-left: 1em; + padding-left: 0.5em; +} + +.comfy-markdown .tiptap pre { + border: thin dotted; + border-radius: 0.5em; + margin: 0.5em; + padding: 0.5em; +} + +.comfy-markdown .tiptap table { + border-collapse: collapse; +} + +.comfy-markdown .tiptap th { + text-align: left; + background: var(--comfy-menu-bg); +} + +.comfy-markdown .tiptap th, +.comfy-markdown .tiptap td { + padding: 0.5em; + border: thin solid; +} + +/* Shared markdown content styling for consistent rendering across components */ +.comfy-markdown-content { + /* Typography */ + font-size: 0.875rem; /* text-sm */ + line-height: 1.6; + word-wrap: break-word; +} + +/* Headings */ +.comfy-markdown-content h1 { + font-size: 22px; /* text-[22px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h1:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h2 { + font-size: 18px; /* text-[18px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h2:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h3 { + font-size: 16px; /* text-[16px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h3:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h4, +.comfy-markdown-content h5, +.comfy-markdown-content h6 { + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h4:first-child, +.comfy-markdown-content h5:first-child, +.comfy-markdown-content h6:first-child { + margin-top: 0; /* first:mt-0 */ +} + +/* Paragraphs */ +.comfy-markdown-content p { + margin: 0 0 0.5em; +} + +.comfy-markdown-content p:last-child { + margin-bottom: 0; +} + +/* First child reset */ +.comfy-markdown-content *:first-child { + margin-top: 0; /* mt-0 */ +} + +/* Lists */ +.comfy-markdown-content ul, +.comfy-markdown-content ol { + padding-left: 2rem; /* pl-8 */ + margin: 0.5rem 0; /* my-2 */ +} + +/* Nested lists */ +.comfy-markdown-content ul ul, +.comfy-markdown-content ol ol, +.comfy-markdown-content ul ol, +.comfy-markdown-content ol ul { + padding-left: 1.5rem; /* pl-6 */ + margin: 0.5rem 0; /* my-2 */ +} + +.comfy-markdown-content li { + margin: 0.5rem 0; /* my-2 */ +} + +/* Code */ +.comfy-markdown-content code { + color: var(--code-text-color); + background-color: var(--code-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */ + font-family: monospace; +} + +.comfy-markdown-content pre { + background-color: var(--code-block-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 1rem; /* p-4 */ + margin: 1rem 0; /* my-4 */ + overflow-x: auto; /* overflow-x-auto */ +} + +.comfy-markdown-content pre code { + background-color: transparent; /* bg-transparent */ + padding: 0; /* p-0 */ + color: var(--p-text-color); +} + +/* Tables */ +.comfy-markdown-content table { + width: 100%; /* w-full */ + border-collapse: collapse; /* border-collapse */ +} + +.comfy-markdown-content th, +.comfy-markdown-content td { + padding: 0.5rem; /* px-2 py-2 */ +} + +.comfy-markdown-content th { + color: var(--fg-color); +} + +.comfy-markdown-content td { + color: var(--drag-text); +} + +.comfy-markdown-content tr { + border-bottom: 1px solid var(--content-bg); +} + +.comfy-markdown-content tr:last-child { + border-bottom: none; +} + +.comfy-markdown-content thead { + border-bottom: 1px solid var(--p-text-color); +} + +/* Links */ +.comfy-markdown-content a { + color: var(--drag-text); + text-decoration: underline; +} + +/* Media */ +.comfy-markdown-content img, +.comfy-markdown-content video { + max-width: 100%; /* max-w-full */ + height: auto; /* h-auto */ + display: block; /* block */ + margin-bottom: 1rem; /* mb-4 */ +} + +/* Blockquotes */ +.comfy-markdown-content blockquote { + border-left: 3px solid var(--p-primary-color, var(--primary-bg)); + padding-left: 0.75em; + margin: 0.5em 0; + opacity: 0.8; +} + +/* Horizontal rule */ +.comfy-markdown-content hr { + border: none; + border-top: 1px solid var(--p-border-color, var(--border-color)); + margin: 1em 0; +} + +/* Strong and emphasis */ +.comfy-markdown-content strong { + font-weight: bold; +} + +.comfy-markdown-content em { + font-style: italic; +} + +.comfy-modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 100; /* Sit on top */ + padding: 30px 30px 10px 30px; + background-color: var(--comfy-menu-bg); /* Modal background */ + color: var(--error-text); + box-shadow: 0 0 20px #888888; + border-radius: 10px; + top: 50%; + left: 50%; + max-width: 80vw; + max-height: 80vh; + transform: translate(-50%, -50%); + overflow: hidden; + justify-content: center; + font-family: monospace; + font-size: 15px; +} + +.comfy-modal-content { + display: flex; + flex-direction: column; +} + +.comfy-modal p { + overflow: auto; + white-space: pre-line; /* This will respect line breaks */ + margin-bottom: 20px; /* Add some margin between the text and the close button*/ +} + +.comfy-modal select, +.comfy-modal input[type='button'], +.comfy-modal input[type='checkbox'] { + margin: 3px 3px 3px 4px; +} + +.comfy-menu { + font-size: 15px; + position: absolute; + top: 50%; + right: 0; + text-align: center; + z-index: 999; + width: 190px; + display: flex; + flex-direction: column; + align-items: center; + color: var(--descrip-text); + background-color: var(--comfy-menu-bg); + font-family: sans-serif; + padding: 10px; + border-radius: 0 8px 8px 8px; + box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4); +} + +.comfy-menu-header { + display: flex; +} + +.comfy-menu-actions { + display: flex; + gap: 3px; + align-items: center; + height: 20px; + position: relative; + top: -1px; + font-size: 22px; +} + +.comfy-menu .comfy-menu-actions button { + background-color: rgba(0, 0, 0, 0); + padding: 0; + border: none; + cursor: pointer; + font-size: inherit; +} + +.comfy-menu .comfy-menu-actions .comfy-settings-btn { + font-size: 0.6em; +} + +button.comfy-close-menu-btn { + font-size: 1em; + line-height: 12px; + color: #ccc; + position: relative; + top: -1px; +} + +.comfy-menu-queue-size { + flex: auto; +} + +.comfy-menu button, +.comfy-modal button { + font-size: 20px; +} + +.comfy-menu-btns { + margin-bottom: 10px; + width: 100%; +} + +.comfy-menu-btns button { + font-size: 10px; + width: 50%; + color: var(--descrip-text) !important; +} + +.comfy-menu > button { + width: 100%; +} + +.comfy-btn, +.comfy-menu > button, +.comfy-menu-btns button, +.comfy-menu .comfy-list button, +.comfy-modal button { + color: var(--input-text); + background-color: var(--comfy-input-bg); + border-width: initial; + border-radius: 8px; + border-color: var(--border-color); + border-style: solid; + margin-top: 2px; +} + +.comfy-btn:hover:not(:disabled), +.comfy-menu > button:hover, +.comfy-menu-btns button:hover, +.comfy-menu .comfy-list button:hover, +.comfy-modal button:hover, +.comfy-menu-actions button:hover { + filter: brightness(1.2); + will-change: transform; + cursor: pointer; +} + +span.drag-handle { + width: 10px; + height: 20px; + display: inline-block; + overflow: hidden; + line-height: 5px; + padding: 3px 4px; + cursor: move; + vertical-align: middle; + margin-top: -0.4em; + margin-left: -0.2em; + font-size: 12px; + font-family: sans-serif; + letter-spacing: 2px; + color: var(--drag-text); + text-shadow: 1px 0 1px black; + touch-action: none; +} + +span.drag-handle::after { + content: '.. .. ..'; +} + +.comfy-queue-btn { + width: 100%; +} + +.comfy-list { + color: var(--descrip-text); + background-color: var(--comfy-menu-bg); + margin-bottom: 10px; + border-color: var(--border-color); + border-style: solid; +} + +.comfy-list-items { + overflow-y: scroll; + max-height: 100px; + min-height: 25px; + background-color: var(--comfy-input-bg); + padding: 5px; +} + +.comfy-list h4 { + min-width: 160px; + margin: 0; + padding: 3px; + font-weight: normal; +} + +.comfy-list-items button { + font-size: 10px; +} + +.comfy-list-actions { + margin: 5px; + display: flex; + gap: 5px; + justify-content: center; +} + +.comfy-list-actions button { + font-size: 12px; +} + +button.comfy-queue-btn { + margin: 6px 0 !important; +} + +.comfy-modal.comfy-settings, +.comfy-modal.comfy-manage-templates { + text-align: center; + font-family: sans-serif; + color: var(--descrip-text); + z-index: 99; +} + +.comfy-modal.comfy-settings input[type='range'] { + vertical-align: middle; +} + +.comfy-modal.comfy-settings input[type='range'] + input[type='number'] { + width: 3.5em; +} + +.comfy-modal input, +.comfy-modal select { + color: var(--input-text); + background-color: var(--comfy-input-bg); + border-radius: 8px; + border-color: var(--border-color); + border-style: solid; + font-size: inherit; +} + +.comfy-tooltip-indicator { + text-decoration: underline; + text-decoration-style: dashed; +} + +@media only screen and (max-height: 850px) { + .comfy-menu { + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + border-radius: 0; + } + + .comfy-menu span.drag-handle { + display: none; + } + + .comfy-menu-queue-size { + flex: unset; + } + + .comfy-menu-header { + justify-content: space-between; + } + .comfy-menu-actions { + gap: 10px; + font-size: 28px; + } +} + +/* Input popup */ + +.graphdialog { + min-height: 1em; + background-color: var(--comfy-menu-bg); + z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */ +} + +.graphdialog .name { + font-size: 14px; + font-family: sans-serif; + color: var(--descrip-text); +} + +.graphdialog button { + margin-top: unset; + vertical-align: unset; + height: 1.6em; + padding-right: 8px; +} + +.graphdialog input, +.graphdialog textarea, +.graphdialog select { + background-color: var(--comfy-input-bg); + border: 2px solid; + border-color: var(--border-color); + color: var(--input-text); + border-radius: 12px 0 0 12px; +} + +/* Dialogs */ + +dialog { + box-shadow: 0 0 20px #888888; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.5); +} + +.comfy-dialog.comfyui-dialog.comfy-modal { + top: 0; + left: 0; + right: 0; + bottom: 0; + transform: none; +} + +.comfy-dialog.comfy-modal { + font-family: Arial, sans-serif; + border-color: var(--bg-color); + box-shadow: none; + border: 2px solid var(--border-color); +} + +.comfy-dialog .comfy-modal-content { + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + color: var(--fg-color); +} + +.comfy-dialog .comfy-modal-content h3 { + margin-top: 0; +} + +.comfy-dialog .comfy-modal-content > p { + width: 100%; +} + +.comfy-dialog .comfy-modal-content > .comfyui-button { + flex: 1; + justify-content: center; +} + +/* Context menu */ + +.litegraph .dialog { + z-index: 1; + font-family: Arial, sans-serif; +} + +.litegraph .litemenu-entry.has_submenu { + position: relative; + padding-right: 20px; +} + +.litemenu-entry.has_submenu::after { + content: '>'; + position: absolute; + top: 0; + right: 2px; +} + +.litegraph.litecontextmenu, +.litegraph.litecontextmenu.dark { + z-index: 9999 !important; + background-color: var(--comfy-menu-bg) !important; +} + +.litegraph.litecontextmenu + .litemenu-entry:hover:not(.disabled):not(.separator) { + background-color: var(--comfy-menu-hover-bg, var(--border-color)) !important; + color: var(--fg-color); +} + +.litegraph.litecontextmenu .litemenu-entry.submenu, +.litegraph.litecontextmenu.dark .litemenu-entry.submenu { + background-color: var(--comfy-menu-bg) !important; + color: var(--input-text); +} + +.litegraph.litecontextmenu input { + background-color: var(--comfy-input-bg) !important; + color: var(--input-text) !important; +} + +.comfy-context-menu-filter { + box-sizing: border-box; + border: 1px solid #999; + margin: 0 0 5px 5px; + width: calc(100% - 10px); +} + +.comfy-img-preview { + pointer-events: none; + overflow: hidden; + display: flex; + flex-wrap: wrap; + align-content: flex-start; + justify-content: center; +} + +.comfy-img-preview img { + object-fit: contain; + width: var(--comfy-img-preview-width); + height: var(--comfy-img-preview-height); +} + +.comfy-img-preview video { + pointer-events: auto; + object-fit: contain; + height: 100%; + width: 100%; +} + +.comfy-missing-nodes li button { + font-size: 12px; + margin-left: 5px; +} + +/* Search box */ + +.litegraph.litesearchbox { + z-index: 9999 !important; + background-color: var(--comfy-menu-bg) !important; + overflow: hidden; + display: block; +} + +.litegraph.litesearchbox input, +.litegraph.litesearchbox select { + background-color: var(--comfy-input-bg) !important; + color: var(--input-text); +} + +.litegraph.lite-search-item { + color: var(--input-text); + background-color: var(--comfy-input-bg); + filter: brightness(80%); + will-change: transform; + padding-left: 0.2em; +} + +.litegraph.lite-search-item.generic_type { + color: var(--input-text); + filter: brightness(50%); + will-change: transform; +} + +audio.comfy-audio.empty-audio-widget { + display: none; +} + +#vue-app { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Set auto complete panel's width as it is not accessible within vue-root */ +.p-autocomplete-overlay { + max-width: 25vw; +} + +.p-tree-node-content { + padding: var(--comfy-tree-explorer-item-padding) !important; +} + +/* Load3d styles */ +.comfy-load-3d, +.comfy-load-3d-animation, +.comfy-preview-3d, +.comfy-preview-3d-animation { + display: flex; + flex-direction: column; + background: transparent; + flex: 1; + position: relative; + overflow: hidden; +} + +.comfy-load-3d canvas, +.comfy-load-3d-animation canvas, +.comfy-preview-3d canvas, +.comfy-preview-3d-animation canvas, +.comfy-load-3d-viewer canvas { + display: flex; + width: 100% !important; + height: 100% !important; +} + +/* End of Load3d styles */ + +/* [Desktop] Electron window specific styles */ +.app-drag { + app-region: drag; +} + +.no-drag { + app-region: no-drag; +} + +.window-actions-spacer { + width: calc(100vw - env(titlebar-area-width, 100vw)); +} +/* End of [Desktop] Electron window specific styles */ + +.lg-node { + /* Disable text selection on all nodes */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.lg-node .lg-slot, +.lg-node .lg-widget { + transition: + opacity 0.1s ease, + font-size 0.1s ease; +} + +/* Performance optimization during canvas interaction */ +.transform-pane--interacting .lg-node * { + transition: none !important; +} + +.transform-pane--interacting .lg-node { + will-change: transform; +} + +/* START LOD specific styles */ +/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */ + +.isLOD .lg-node { + box-shadow: none; + filter: none; + backdrop-filter: none; + text-shadow: none; + -webkit-mask-image: none; + mask-image: none; + clip-path: none; + background-image: none; + text-rendering: optimizeSpeed; + border-radius: 0; + contain: layout style; + transition: none; +} + +.isLOD .lg-node-widgets { + pointer-events: none; +} + +.lod-toggle { + visibility: visible; +} + +.isLOD .lod-toggle { + visibility: hidden; +} + +.lod-fallback { + display: none; +} + +.isLOD .lod-fallback { + display: block; +} + +.isLOD .image-preview img { + image-rendering: pixelated; +} + +.isLOD .slot-dot { + border-radius: 0; +} +/* END LOD specific styles */ diff --git a/build/customIconCollection.ts b/packages/design-system/src/iconCollection.ts similarity index 96% rename from build/customIconCollection.ts rename to packages/design-system/src/iconCollection.ts index f2d823ed5d..170a5465fb 100644 --- a/build/customIconCollection.ts +++ b/packages/design-system/src/iconCollection.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from 'url' const fileName = fileURLToPath(import.meta.url) const dirName = dirname(fileName) -const customIconsPath = join(dirName, '..', 'src', 'assets', 'icons', 'custom') +const customIconsPath = join(dirName, 'icons') // Iconify collection structure interface IconifyIcon { diff --git a/src/assets/icons/README.md b/packages/design-system/src/icons/README.md similarity index 95% rename from src/assets/icons/README.md rename to packages/design-system/src/icons/README.md index b01a3e3ef6..ba7cdb3e49 100644 --- a/src/assets/icons/README.md +++ b/packages/design-system/src/icons/README.md @@ -51,7 +51,7 @@ ComfyUI supports three types of icons that can be used throughout the interface. ```vue