diff --git a/.github/workflows/update-locales.yaml b/.github/workflows/update-locales.yaml index a612130a7..9ffa702ca 100644 --- a/.github/workflows/update-locales.yaml +++ b/.github/workflows/update-locales.yaml @@ -18,14 +18,14 @@ jobs: uses: actions/checkout@v5 # Setup playwright environment - - name: Setup ComfyUI Server - uses: ./.github/actions/setup-comfyui-server - with: - launch_server: true - name: Setup ComfyUI Frontend uses: ./.github/actions/setup-frontend with: include_build_step: true + - name: Setup ComfyUI Server + uses: ./.github/actions/setup-comfyui-server + with: + launch_server: true - name: Setup Playwright uses: ./.github/actions/setup-playwright diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 19796f4c4..d46b31a98 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1,6 +1,5 @@ import type { APIRequestContext, Locator, Page } from '@playwright/test' -import { expect } from '@playwright/test' -import { test as base } from '@playwright/test' +import { test as base, expect } from '@playwright/test' import dotenv from 'dotenv' import * as fs from 'fs' @@ -130,7 +129,8 @@ export class ComfyPage { // Buttons public readonly resetViewButton: Locator - public readonly queueButton: Locator + public readonly queueButton: Locator // Run button in Legacy UI + public readonly runButton: Locator // Run button (renamed "Queue" -> "Run") // Inputs public readonly workflowUploadInput: Locator @@ -165,6 +165,9 @@ export class ComfyPage { this.widgetTextBox = page.getByPlaceholder('text').nth(1) this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) + this.runButton = page + .getByTestId('queue-button') + .getByRole('button', { name: 'Run' }) this.workflowUploadInput = page.locator('#comfy-file-input') this.visibleToasts = page.locator('.p-toast-message:visible') @@ -1086,12 +1089,6 @@ export class ComfyPage { const targetPosition = await targetSlot.getPosition() - // Debug: Log the positions we're trying to use - console.log('Drag positions:', { - source: sourcePosition, - target: targetPosition - }) - await this.dragAndDrop(sourcePosition, targetPosition) await this.nextFrame() } diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index 64c03b156..86d715bfd 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -3,6 +3,8 @@ */ import type { Locator, Page } from '@playwright/test' +import { VueNodeFixture } from './utils/vueNodeFixtures' + export class VueNodeHelpers { constructor(private page: Page) {} @@ -106,6 +108,24 @@ export class VueNodeHelpers { await this.page.keyboard.press('Backspace') } + /** + * Return a DOM-focused VueNodeFixture for the first node matching the title. + * Resolves the node id up front so subsequent interactions survive title changes. + */ + async getFixtureByTitle(title: string): Promise { + const node = this.getNodeByTitle(title).first() + await node.waitFor({ state: 'visible' }) + + const nodeId = await node.evaluate((el) => el.getAttribute('data-node-id')) + if (!nodeId) { + throw new Error( + `Vue node titled "${title}" is missing its data-node-id attribute` + ) + } + + return new VueNodeFixture(this.getNodeLocator(nodeId)) + } + /** * Wait for Vue nodes to be rendered */ diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts index 5c4541b92..fca464405 100644 --- a/browser_tests/fixtures/utils/vueNodeFixtures.ts +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -1,131 +1,66 @@ -import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' +import type { Locator } from '@playwright/test' -import type { NodeReference } from './litegraphUtils' - -/** - * VueNodeFixture provides Vue-specific testing utilities for interacting with - * Vue node components. It bridges the gap between litegraph node references - * and Vue UI components. - */ +/** DOM-centric helper for a single Vue-rendered node on the canvas. */ export class VueNodeFixture { - constructor( - private readonly nodeRef: NodeReference, - private readonly page: Page - ) {} + constructor(private readonly locator: Locator) {} - /** - * Get the node's header element using data-testid - */ - async getHeader(): Promise { - const nodeId = this.nodeRef.id - return this.page.locator(`[data-testid="node-header-${nodeId}"]`) + get header(): Locator { + return this.locator.locator('[data-testid^="node-header-"]') } - /** - * Get the node's title element - */ - async getTitleElement(): Promise { - const header = await this.getHeader() - return header.locator('[data-testid="node-title"]') + get title(): Locator { + return this.locator.locator('[data-testid="node-title"]') + } + + get titleInput(): Locator { + return this.locator.locator('[data-testid="node-title-input"]') + } + + get body(): Locator { + return this.locator.locator('[data-testid^="node-body-"]') + } + + get collapseButton(): Locator { + return this.locator.locator('[data-testid="node-collapse-button"]') + } + + get collapseIcon(): Locator { + return this.collapseButton.locator('i') + } + + get root(): Locator { + return this.locator } - /** - * Get the current title text - */ async getTitle(): Promise { - const titleElement = await this.getTitleElement() - return (await titleElement.textContent()) || '' + return (await this.title.textContent()) ?? '' } - /** - * Set a new title by double-clicking and entering text - */ - async setTitle(newTitle: string): Promise { - const titleElement = await this.getTitleElement() - await titleElement.dblclick() - - const input = (await this.getHeader()).locator( - '[data-testid="node-title-input"]' - ) - await input.fill(newTitle) + async setTitle(value: string): Promise { + await this.header.dblclick() + const input = this.titleInput + await expect(input).toBeVisible() + await input.fill(value) await input.press('Enter') } - /** - * Cancel title editing - */ async cancelTitleEdit(): Promise { - const titleElement = await this.getTitleElement() - await titleElement.dblclick() - - const input = (await this.getHeader()).locator( - '[data-testid="node-title-input"]' - ) + await this.header.dblclick() + const input = this.titleInput + await expect(input).toBeVisible() await input.press('Escape') } - /** - * Check if the title is currently being edited - */ - async isEditingTitle(): Promise { - const header = await this.getHeader() - const input = header.locator('[data-testid="node-title-input"]') - return await input.isVisible() - } - - /** - * Get the collapse/expand button - */ - async getCollapseButton(): Promise { - const header = await this.getHeader() - return header.locator('[data-testid="node-collapse-button"]') - } - - /** - * Toggle the node's collapsed state - */ async toggleCollapse(): Promise { - const button = await this.getCollapseButton() - await button.click() + await this.collapseButton.click() } - /** - * Get the collapse icon element - */ - async getCollapseIcon(): Promise { - const button = await this.getCollapseButton() - return button.locator('i') - } - - /** - * Get the collapse icon's CSS classes - */ async getCollapseIconClass(): Promise { - const icon = await this.getCollapseIcon() - return (await icon.getAttribute('class')) || '' + return (await this.collapseIcon.getAttribute('class')) ?? '' } - /** - * Check if the collapse button is visible - */ - async isCollapseButtonVisible(): Promise { - const button = await this.getCollapseButton() - return await button.isVisible() - } - - /** - * Get the node's body/content element - */ - async getBody(): Promise { - const nodeId = this.nodeRef.id - return this.page.locator(`[data-testid="node-body-${nodeId}"]`) - } - - /** - * Check if the node body is visible (not collapsed) - */ - async isBodyVisible(): Promise { - const body = await this.getBody() - return await body.isVisible() + boundingBox(): ReturnType { + return this.locator.boundingBox() } } diff --git a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png index 02c2e31a2..cb8e7c17f 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/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index daa165fa4..fc8385e49 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -39,15 +39,15 @@ test.describe('Graph Canvas Menu', () => { ) }) - test('Focus mode button is clickable and has correct test id', async ({ + test('Toggle minimap button is clickable and has correct test id', async ({ comfyPage }) => { - const focusButton = comfyPage.page.getByTestId('focus-mode-button') - await expect(focusButton).toBeVisible() - await expect(focusButton).toBeEnabled() + const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button') + await expect(minimapButton).toBeVisible() + await expect(minimapButton).toBeEnabled() // Test that the button can be clicked without error - await focusButton.click() + await minimapButton.click() await comfyPage.nextFrame() }) 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 2736a50c5..382a7d03f 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 fd72c2d0a..8a87725f3 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/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index 2fc753490..d1afc696f 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -3,10 +3,10 @@ import { expect } from '@playwright/test' import type { Position } from '@vueuse/core' import { - type ComfyPage, comfyPageFixture as test, testComfySnapToGridGridSize } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { @@ -786,16 +786,8 @@ test.describe('Viewport settings', () => { // Screenshot the canvas element await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) - // Open zoom controls dropdown first - const zoomControlsButton = comfyPage.page.getByTestId( - 'zoom-controls-button' - ) - await zoomControlsButton.click() - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') await toggleButton.click() - // close zoom menu - await zoomControlsButton.click() await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.menu.topbar.saveWorkflow('Workflow A') diff --git a/browser_tests/tests/minimap.spec.ts b/browser_tests/tests/minimap.spec.ts index d1ab05fc5..cea5fe9a8 100644 --- a/browser_tests/tests/minimap.spec.ts +++ b/browser_tests/tests/minimap.spec.ts @@ -35,12 +35,6 @@ test.describe('Minimap', () => { }) test('Validate minimap toggle button state', async ({ comfyPage }) => { - // Open zoom controls dropdown first - const zoomControlsButton = comfyPage.page.getByTestId( - 'zoom-controls-button' - ) - await zoomControlsButton.click() - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') await expect(toggleButton).toBeVisible() @@ -51,13 +45,6 @@ test.describe('Minimap', () => { test('Validate minimap can be toggled off and on', async ({ comfyPage }) => { const minimapContainer = comfyPage.page.locator('.litegraph-minimap') - - // Open zoom controls dropdown first - const zoomControlsButton = comfyPage.page.getByTestId( - 'zoom-controls-button' - ) - await zoomControlsButton.click() - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') await expect(minimapContainer).toBeVisible() @@ -67,22 +54,10 @@ test.describe('Minimap', () => { await expect(minimapContainer).not.toBeVisible() - // Open zoom controls dropdown again - await zoomControlsButton.click() - await comfyPage.nextFrame() - - await expect(toggleButton).toContainText('Show Minimap') - await toggleButton.click() await comfyPage.nextFrame() await expect(minimapContainer).toBeVisible() - - // Open zoom controls dropdown again to verify button text - await zoomControlsButton.click() - await comfyPage.nextFrame() - - await expect(toggleButton).toContainText('Hide Minimap') }) test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => { 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 d66ea8927..2dca958d5 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/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png index 96f6507e1..bcc9e7e30 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png index af92221f3..ce3118790 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png index f9b9b012c..3e804b0ab 100644 Binary files a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png index 2548e66ae..74c9f0b4b 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index 9377c7e7a..b5583be8f 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index ec0efa7b5..2091d962f 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index e1b527186..f1b14b491 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index 15065b8f2..e4edbc459 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index 8989dc632..2da4b1122 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -60,7 +60,6 @@ async function getInputLinkDetails( ) } -// Test helpers to reduce repetition across cases function slotLocator( page: Page, nodeId: NodeId, @@ -789,6 +788,45 @@ test.describe('Vue Node Link Interaction', () => { }) }) + test('should batch disconnect all links with ctrl+alt+click on slot', async ({ + comfyPage + }) => { + const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] + const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + expect(clipNode && samplerNode).toBeTruthy() + + await connectSlots( + comfyPage.page, + { nodeId: clipNode.id, index: 0 }, + { nodeId: samplerNode.id, index: 1 }, + () => comfyPage.nextFrame() + ) + await connectSlots( + comfyPage.page, + { nodeId: clipNode.id, index: 0 }, + { nodeId: samplerNode.id, index: 2 }, + () => comfyPage.nextFrame() + ) + + const clipOutput = await clipNode.getOutput(0) + expect(await clipOutput.getLinkCount()).toBe(2) + + const clipOutputSlot = slotLocator(comfyPage.page, clipNode.id, 0, false) + + await clipOutputSlot.dispatchEvent('pointerdown', { + button: 0, + buttons: 1, + ctrlKey: true, + altKey: true, + shiftKey: false, + bubbles: true, + cancelable: true + }) + await comfyPage.nextFrame() + + expect(await clipOutput.getLinkCount()).toBe(0) + }) + test.describe('Release actions (Shift-drop)', () => { test('Context menu opens and endpoint is pinned on Shift-drop', async ({ comfyPage, diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 9a240d6f5..49d875ac2 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 55467f622..b3dd3c9e3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 2e7b63cb7..f923dd0f4 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index 0d679ac5e..c55cf2f89 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 23ada3e2d..051c2e3ee 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index fa12ec0f4..62d043713 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index d810cdb9c..625caa9af 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 27b4fe6f8..465a9e7c0 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index e0a53ef0c..60ab7ea0d 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index d4ff35400..dd727958e 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts index 3984989e1..5cd6b2fea 100644 --- a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts @@ -2,70 +2,46 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' -import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures' test.describe('Vue Nodes Renaming', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() + await comfyPage.vueNodes.waitForNodes() }) test('should display node title', async ({ comfyPage }) => { - // Get the KSampler node from the default workflow - const nodes = await comfyPage.getNodeRefsByType('KSampler') - expect(nodes.length).toBeGreaterThanOrEqual(1) - - const node = nodes[0] - const vueNode = new VueNodeFixture(node, comfyPage.page) - - const title = await vueNode.getTitle() - expect(title).toBe('KSampler') - - // Verify title is visible in the header - const header = await vueNode.getHeader() - await expect(header).toContainText('KSampler') + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.header).toContainText('KSampler') }) test('should allow title renaming by double clicking on the node header', async ({ comfyPage }) => { - const nodes = await comfyPage.getNodeRefsByType('KSampler') - const node = nodes[0] - const vueNode = new VueNodeFixture(node, comfyPage.page) - + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') // Test renaming with Enter await vueNode.setTitle('My Custom Sampler') - const newTitle = await vueNode.getTitle() - expect(newTitle).toBe('My Custom Sampler') - - // Verify the title is displayed - const header = await vueNode.getHeader() - await expect(header).toContainText('My Custom Sampler') + await expect(await vueNode.getTitle()).toBe('My Custom Sampler') + await expect(vueNode.header).toContainText('My Custom Sampler') // Test cancel with Escape - const titleElement = await vueNode.getTitleElement() - await titleElement.dblclick() + await vueNode.title.dblclick() await comfyPage.nextFrame() - - // Type a different value but cancel - const input = (await vueNode.getHeader()).locator( - '[data-testid="node-title-input"]' - ) - await input.fill('This Should Be Cancelled') - await input.press('Escape') + await vueNode.titleInput.fill('This Should Be Cancelled') + await vueNode.titleInput.press('Escape') await comfyPage.nextFrame() // Title should remain as the previously saved value - const titleAfterCancel = await vueNode.getTitle() - expect(titleAfterCancel).toBe('My Custom Sampler') + await expect(await vueNode.getTitle()).toBe('My Custom Sampler') }) test('Double click node body does not trigger edit', async ({ comfyPage }) => { - const loadCheckpointNode = - comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const loadCheckpointNode = comfyPage.vueNodes + .getNodeByTitle('Load Checkpoint') + .first() const nodeBbox = await loadCheckpointNode.boundingBox() if (!nodeBbox) throw new Error('Node not found') await loadCheckpointNode.dblclick() diff --git a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts index 4402236d2..98b0a63f6 100644 --- a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts @@ -50,15 +50,23 @@ test.describe('Vue Node Selection', () => { }) } + test('should select all nodes with ctrl+a', async ({ comfyPage }) => { + const initialCount = await comfyPage.vueNodes.getNodeCount() + expect(initialCount).toBeGreaterThan(0) + + await comfyPage.canvas.press('Control+a') + + const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() + expect(selectedCount).toBe(initialCount) + }) + test('should select pinned node without dragging', async ({ comfyPage }) => { const PIN_HOTKEY = 'p' const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' - // Select a node by clicking its title const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint') await checkpointNodeHeader.click() - // Pin it using the hotkey (as a user would) await comfyPage.page.keyboard.press(PIN_HOTKEY) const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts index 74ec17cc9..2d67ca99f 100644 --- a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -20,6 +20,9 @@ test.describe('Vue Node Bypass', () => { const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-bypassed-state.png' + ) await comfyPage.page.keyboard.press(BYPASS_HOTKEY) await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png new file mode 100644 index 000000000..b0d14bfa8 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts index fb5fc3c17..8e8e22995 100644 --- a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts @@ -2,7 +2,6 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../fixtures/ComfyPage' -import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures' test.describe('Vue Node Collapse', () => { test.beforeEach(async ({ comfyPage }) => { @@ -10,43 +9,50 @@ test.describe('Vue Node Collapse', () => { await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() + await comfyPage.vueNodes.waitForNodes() }) test('should allow collapsing node with collapse icon', async ({ comfyPage }) => { - // Get the KSampler node from the default workflow - const nodes = await comfyPage.getNodeRefsByType('KSampler') - const node = nodes[0] - const vueNode = new VueNodeFixture(node, comfyPage.page) + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.root).toBeVisible() // Initially should not be collapsed - expect(await node.isCollapsed()).toBe(false) - const body = await vueNode.getBody() + const body = vueNode.body await expect(body).toBeVisible() + const expandedBoundingBox = await vueNode.boundingBox() + if (!expandedBoundingBox) + throw new Error('Failed to get node bounding box before collapse') // Collapse the node await vueNode.toggleCollapse() - expect(await node.isCollapsed()).toBe(true) + await comfyPage.nextFrame() // Verify node content is hidden - const collapsedSize = await node.getSize() await expect(body).not.toBeVisible() + const collapsedBoundingBox = await vueNode.boundingBox() + if (!collapsedBoundingBox) + throw new Error('Failed to get node bounding box after collapse') + expect(collapsedBoundingBox.height).toBeLessThan(expandedBoundingBox.height) // Expand again await vueNode.toggleCollapse() - expect(await node.isCollapsed()).toBe(false) + await comfyPage.nextFrame() await expect(body).toBeVisible() // Size should be restored - const expandedSize = await node.getSize() - expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height) + const expandedBoundingBoxAfter = await vueNode.boundingBox() + if (!expandedBoundingBoxAfter) + throw new Error('Failed to get node bounding box after expand') + expect(expandedBoundingBoxAfter.height).toBeGreaterThanOrEqual( + collapsedBoundingBox.height + ) }) test('should show collapse/expand icon state', async ({ comfyPage }) => { - const nodes = await comfyPage.getNodeRefsByType('KSampler') - const node = nodes[0] - const vueNode = new VueNodeFixture(node, comfyPage.page) + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.root).toBeVisible() // Check initial expanded state icon let iconClass = await vueNode.getCollapseIconClass() @@ -66,9 +72,8 @@ test.describe('Vue Node Collapse', () => { test('should preserve title when collapsing/expanding', async ({ comfyPage }) => { - const nodes = await comfyPage.getNodeRefsByType('KSampler') - const node = nodes[0] - const vueNode = new VueNodeFixture(node, comfyPage.page) + const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler') + await expect(vueNode.root).toBeVisible() // Set custom title await vueNode.setTitle('Test Sampler') @@ -83,7 +88,6 @@ test.describe('Vue Node Collapse', () => { expect(await vueNode.getTitle()).toBe('Test Sampler') // Verify title is still displayed - const header = await vueNode.getHeader() - await expect(header).toContainText('Test Sampler') + await expect(vueNode.header).toContainText('Test Sampler') }) }) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index a82bc9297..d75aaa7ad 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index 9680ae0f2..ca65a1de1 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png index e1b517c95..0485a92d4 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts index f4f8e10fe..b8c718239 100644 --- a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts @@ -3,7 +3,7 @@ import { comfyPageFixture as test } from '../../../fixtures/ComfyPage' -const ERROR_CLASS = /border-error/ +const ERROR_CLASS = /border-node-stroke-error/ test.describe('Vue Node Error', () => { test.beforeEach(async ({ comfyPage }) => { @@ -17,16 +17,21 @@ test.describe('Vue Node Error', () => { await comfyPage.setup() await comfyPage.loadWorkflow('missing/missing_nodes') - // Close missing nodes warning dialog - await comfyPage.page.getByRole('button', { name: 'Close' }).click() - await comfyPage.page.waitForSelector('.comfy-missing-nodes', { - state: 'hidden' - }) - // Expect error state on missing unknown node const unknownNode = comfyPage.page.locator('[data-node-id]').filter({ hasText: 'UNKNOWN NODE' }) await expect(unknownNode).toHaveClass(ERROR_CLASS) }) + + test('should display error state when node causes execution error', async ({ + comfyPage + }) => { + await comfyPage.setup() + await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.runButton.click() + + const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error') + await expect(raiseErrorNode).toHaveClass(ERROR_CLASS) + }) }) diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png index 347f432b6..795cc4c8d 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png index 6ca10feb4..e2552e33b 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts index 37dcfd37b..3fe656ebc 100644 --- a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -4,7 +4,7 @@ import { } from '../../../fixtures/ComfyPage' const MUTE_HOTKEY = 'Control+m' -const MUTE_CLASS = /opacity-50/ +const MUTE_OPACITY = '0.5' test.describe('Vue Node Mute', () => { test.beforeEach(async ({ comfyPage }) => { @@ -19,10 +19,11 @@ test.describe('Vue Node Mute', () => { await comfyPage.page.keyboard.press(MUTE_HOTKEY) const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') - await expect(checkpointNode).toHaveClass(MUTE_CLASS) + await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY) + await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png') await comfyPage.page.keyboard.press(MUTE_HOTKEY) - await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) + await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY) }) test('should allow toggling mute on multiple selected nodes with hotkey', async ({ @@ -35,11 +36,11 @@ test.describe('Vue Node Mute', () => { const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') await comfyPage.page.keyboard.press(MUTE_HOTKEY) - await expect(checkpointNode).toHaveClass(MUTE_CLASS) - await expect(ksamplerNode).toHaveClass(MUTE_CLASS) + await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY) + await expect(ksamplerNode).toHaveCSS('opacity', MUTE_OPACITY) await comfyPage.page.keyboard.press(MUTE_HOTKEY) - await expect(checkpointNode).not.toHaveClass(MUTE_CLASS) - await expect(ksamplerNode).not.toHaveClass(MUTE_CLASS) + await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY) + await expect(ksamplerNode).not.toHaveCSS('opacity', MUTE_OPACITY) }) }) diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png new file mode 100644 index 000000000..4018175b1 Binary files /dev/null and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index 2bf70f0d2..1c4bacd6d 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts new file mode 100644 index 000000000..6f3701c12 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts @@ -0,0 +1,51 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.describe('Vue Widget Reactivity', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + test('Should display added widgets', async ({ comfyPage }) => { + const loadCheckpointNode = comfyPage.page.locator( + 'css=[data-testid="node-body-4"] > .lg-node-widgets > div' + ) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets.push(node.widgets[0]) + }) + await expect(loadCheckpointNode).toHaveCount(2) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets[2] = node.widgets[0] + }) + await expect(loadCheckpointNode).toHaveCount(3) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets.splice(0, 0, node.widgets[0]) + }) + await expect(loadCheckpointNode).toHaveCount(4) + }) + test('Should hide removed widgets', async ({ comfyPage }) => { + const loadCheckpointNode = comfyPage.page.locator( + 'css=[data-testid="node-body-3"] > .lg-node-widgets > div' + ) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.pop() + }) + await expect(loadCheckpointNode).toHaveCount(5) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.length-- + }) + await expect(loadCheckpointNode).toHaveCount(4) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.splice(0, 1) + }) + await expect(loadCheckpointNode).toHaveCount(3) + }) +}) diff --git a/package.json b/package.json index fc6194ba8..b28d7b9de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.29.1", + "version": "1.29.2", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -11,6 +11,7 @@ "build:desktop": "nx build @comfyorg/desktop-ui", "build-storybook": "storybook build", "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", + "build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build", "build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build", "collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts", "dev:desktop": "nx dev @comfyorg/desktop-ui", @@ -90,6 +91,7 @@ "nx": "catalog:", "postcss-html": "catalog:", "prettier": "catalog:", + "rollup-plugin-visualizer": "catalog:", "storybook": "catalog:", "stylelint": "catalog:", "tailwindcss": "catalog:", diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index df630d226..769f52d55 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -24,11 +24,14 @@ --text-xxs: 0.625rem; --text-xxs--line-height: calc(1 / 0.625); + /* Spacing */ + --spacing-xs: 8px; + /* Font Families */ --font-inter: 'Inter', sans-serif; /* Palette Colors */ - --color-charcoal-100: #55565e; + --color-charcoal-100: #171718; --color-charcoal-200: #494a50; --color-charcoal-300: #3c3d42; --color-charcoal-400: #313235; @@ -85,10 +88,29 @@ --color-bypass: #6a246a; --color-error: #962a2a; + --color-comfy-menu-secondary: var(--comfy-menu-secondary-bg); + --text-xxxs: 0.5625rem; + --text-xxxs--line-height: calc(1 / 0.5625); + --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); + --color-alpha-charcoal-600-30: color-mix( + in srgb, + var(--color-charcoal-600) 30%, + transparent + ); + --color-alpha-stone-100-20: color-mix( + in srgb, + var(--color-stone-100) 20%, + transparent + ); + --color-alpha-gray-500-50: color-mix( + in srgb, + var(--color-gray-500) 50%, + transparent + ); /* PrimeVue pulled colors */ --color-muted: var(--p-text-muted-color); @@ -129,9 +151,20 @@ /* --- */ + --accent-primary: var(--color-charcoal-700); --backdrop: var(--color-white); + --button-hover-surface: var(--color-gray-200); + --button-active-surface: var(--color-gray-400); --dialog-surface: var(--color-neutral-200); + --interface-menu-component-surface-hovered: var(--color-gray-200); + --interface-menu-component-surface-selected: var(--color-gray-400); + --interface-menu-keybind-surface-default: var(--color-gray-500); + --interface-panel-surface: var(--color-pure-white); + --interface-stroke: var(--color-gray-300); + --nav-background: var(--color-pure-white); + --node-border: var(--color-gray-300); --node-component-border: var(--color-gray-400); + --node-component-disabled: var(--color-alpha-stone-100-20); --node-component-executing: var(--color-blue-500); --node-component-header: var(--fg-color); --node-component-header-icon: var(--color-stone-200); @@ -143,7 +176,7 @@ --node-component-slot-dot-outline: var(--color-black); --node-component-slot-text: var(--color-stone-200); --node-component-surface-highlight: var(--color-stone-100); - --node-component-surface-hovered: var(--color-charcoal-400); + --node-component-surface-hovered: var(--color-gray-200); --node-component-surface-selected: var(--color-charcoal-200); --node-component-surface: var(--color-white); --node-component-tooltip: var(--color-charcoal-700); @@ -154,13 +187,34 @@ from var(--color-zinc-500) r g b / 10% ); --node-component-widget-skeleton-surface: var(--color-zinc-300); - --node-stroke: var(--color-stone-100); + --node-divider: var(--color-sand-100); + --node-icon-disabled: var(--color-alpha-gray-500-50); + --node-stroke: var(--color-gray-400); + --node-stroke-selected: var(--color-accent-primary); + --node-stroke-error: var(--color-error); + --node-stroke-executing: var(--color-blue-100); + --text-secondary: var(--color-stone-100); + --text-primary: var(--color-charcoal-700); + --input-surface: rgba(0, 0, 0, 0.15); } .dark-theme { + --accent-primary: var(--color-pure-white); --backdrop: var(--color-neutral-900); + --button-hover-surface: var(--color-charcoal-600); + --button-active-surface: var(--color-charcoal-600); --dialog-surface: var(--color-neutral-700); + --interface-menu-component-surface-hovered: var(--color-charcoal-400); + --interface-menu-component-surface-selected: var(--color-charcoal-300); + --interface-menu-keybind-surface-default: var(--color-charcoal-200); + --interface-panel-surface: var(--color-charcoal-100); + --interface-stroke: var(--color-charcoal-400); + --nav-background: var(--color-charcoal-100); + --node-border: var(--color-charcoal-500); --node-component-border: var(--color-stone-200); + --node-component-border-error: var(--color-danger-100); + --node-component-border-executing: var(--color-blue-500); + --node-component-border-selected: var(--color-charcoal-200); --node-component-header-icon: var(--color-slate-300); --node-component-header-surface: var(--color-charcoal-800); --node-component-outline: var(--color-white); @@ -169,19 +223,37 @@ --node-component-slot-dot-outline: var(--color-white); --node-component-slot-text: var(--color-slate-200); --node-component-surface-highlight: var(--color-slate-100); - --node-component-surface-hovered: var(--color-charcoal-400); + --node-component-surface-hovered: var(--color-charcoal-600); --node-component-surface-selected: var(--color-charcoal-200); --node-component-surface: var(--color-charcoal-800); --node-component-tooltip: var(--color-white); --node-component-tooltip-border: var(--color-slate-300); --node-component-tooltip-surface: var(--color-charcoal-800); --node-component-widget-skeleton-surface: var(--color-zinc-800); - --node-stroke: var(--color-slate-100); + --node-component-disabled: var(--color-alpha-charcoal-600-30); + --node-divider: var(--color-charcoal-500); + --node-icon-disabled: var(--color-alpha-stone-100-20); + --node-stroke: var(--color-stone-200); + --node-stroke-selected: var(--color-pure-white); + --node-stroke-error: var(--color-error); + --node-stroke-executing: var(--color-blue-100); + --text-secondary: var(--color-slate-100); + --text-primary: var(--color-pure-white); + --input-surface: rgba(130, 130, 130, 0.1); } @theme inline { --color-backdrop: var(--backdrop); + --color-button-hover-surface: var(--button-hover-surface); + --color-button-active-surface: var(--button-active-surface); --color-dialog-surface: var(--dialog-surface); + --color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered); + --color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected); + --color-interface-menu-keybind-surface-default: var(--interface-menu-keybind-surface-default); + --color-interface-panel-surface: var(--interface-panel-surface); + --color-interface-stroke: var(--interface-stroke); + --color-nav-background: var(--nav-background); + --color-node-border: var(--node-border); --color-node-component-border: var(--node-component-border); --color-node-component-executing: var(--node-component-executing); --color-node-component-header: var(--node-component-header); @@ -213,7 +285,16 @@ --color-node-component-widget-skeleton-surface: var( --node-component-widget-skeleton-surface ); + --color-node-component-disabled: var(--node-component-disabled); + --color-node-divider: var(--node-divider); + --color-node-icon-disabled: var(--node-icon-disabled); --color-node-stroke: var(--node-stroke); + --color-node-stroke-selected: var(--node-stroke-selected); + --color-node-stroke-error: var(--node-stroke-error); + --color-node-stroke-executing: var(--node-stroke-executing); + --color-text-secondary: var(--text-secondary); + --color-text-primary: var(--text-primary); + --color-input-surface: var(--input-surface); } @custom-variant dark-theme { diff --git a/packages/design-system/src/icons/play.svg b/packages/design-system/src/icons/play.svg new file mode 100644 index 000000000..19c709083 --- /dev/null +++ b/packages/design-system/src/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/shared-frontend-utils/src/formatUtil.ts b/packages/shared-frontend-utils/src/formatUtil.ts index a5d525c82..658498f1f 100644 --- a/packages/shared-frontend-utils/src/formatUtil.ts +++ b/packages/shared-frontend-utils/src/formatUtil.ts @@ -82,7 +82,7 @@ export function formatSize(value?: number) { * - filename: 'file' * - suffix: 'txt' */ -function getFilenameDetails(fullFilename: string) { +export function getFilenameDetails(fullFilename: string) { if (fullFilename.includes('.')) { return { filename: fullFilename.split('.').slice(0, -1).join('.'), @@ -451,3 +451,26 @@ export function stringToLocale(locale: string): SupportedLocale { ? (locale as SupportedLocale) : 'en' } + +export function formatDuration(milliseconds: number): string { + if (!milliseconds || milliseconds < 0) return '0s' + + const totalSeconds = Math.floor(milliseconds / 1000) + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const remainingSeconds = Math.floor(totalSeconds % 60) + + const parts: string[] = [] + + if (hours > 0) { + parts.push(`${hours}h`) + } + if (minutes > 0) { + parts.push(`${minutes}m`) + } + if (remainingSeconds > 0 || parts.length === 0) { + parts.push(`${remainingSeconds}s`) + } + + return parts.join(' ') +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b362bc15..de05d7d7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,9 @@ catalogs: primevue: specifier: ^4.2.5 version: 4.2.5 + rollup-plugin-visualizer: + specifier: ^6.0.4 + version: 6.0.4 storybook: specifier: ^9.1.6 version: 9.1.6 @@ -591,6 +594,9 @@ importers: prettier: specifier: 'catalog:' version: 3.3.2 + rollup-plugin-visualizer: + specifier: 'catalog:' + version: 6.0.4(rollup@4.22.4) storybook: specifier: 'catalog:' version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) @@ -6668,6 +6674,19 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup-plugin-visualizer@6.0.4: + resolution: {integrity: sha512-q8Q7J/6YofkmaGW1sH/fPRAz37x/+pd7VBuaUU7lwvOS/YikuiiEU9jeb9PH8XHiq50XFrUsBbOxeAMYQ7KZkg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x || ^1.0.0-beta + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + rollup@4.22.4: resolution: {integrity: sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -6822,6 +6841,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -14733,6 +14756,15 @@ snapshots: rfdc@1.4.1: {} + rollup-plugin-visualizer@6.0.4(rollup@4.22.4): + dependencies: + open: 8.4.2 + picomatch: 4.0.3 + source-map: 0.7.6 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.22.4 + rollup@4.22.4: dependencies: '@types/estree': 1.0.5 @@ -14924,6 +14956,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + speakingurl@14.0.1: {} sprintf-js@1.0.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f3b1ce376..10eadc424 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -68,6 +68,7 @@ catalog: prettier: ^3.3.2 primeicons: ^7.0.0 primevue: ^4.2.5 + rollup-plugin-visualizer: ^6.0.4 storybook: ^9.1.6 stylelint: ^16.24.0 tailwindcss: ^4.1.12 diff --git a/src/components/button/IconGroup.vue b/src/components/button/IconGroup.vue index 9c5bbd40c..9e73edfab 100644 --- a/src/components/button/IconGroup.vue +++ b/src/components/button/IconGroup.vue @@ -12,6 +12,7 @@ const iconGroupClasses = cn( 'outline-hidden border-none p-0 rounded-lg', 'bg-white dark-theme:bg-zinc-700', 'text-neutral-950 dark-theme:text-white', + 'transition-all duration-200', 'cursor-pointer' ) diff --git a/src/components/button/MoreButton.vue b/src/components/button/MoreButton.vue index 71693ea3a..2cdf394fe 100644 --- a/src/components/button/MoreButton.vue +++ b/src/components/button/MoreButton.vue @@ -1,7 +1,8 @@