Compare commits

..

15 Commits

Author SHA1 Message Date
Jin Yi
d193f6e6b5 feature: delete asset 2025-10-22 22:59:30 +09:00
Jin Yi
2cfab8a25d refactor: use Queuestore.tasks for grouped media assets 2025-10-22 22:59:21 +09:00
Jin Yi
373266003f chore: icon -> button markup 2025-10-22 22:59:11 +09:00
Jin Yi
ee92a89239 feature: tab storybook added 2025-10-22 22:59:02 +09:00
Jin Yi
d59e8b8513 refactor: Create reusable Tab components and update AssetsSidebarTab UI
- Add new Tab and TabList components with provide/inject pattern for
  state management
  - Replace TextButton-based tabs with new Tab components in
  AssetsSidebarTab
  - Update AssetSidebarTemplate to use semantic color tokens
  (bg-interface-panel-surface)
  - Improve tab styling with proper hover and focus states
2025-10-22 22:58:49 +09:00
Jin Yi
72895a7d41 refactor: Extract AssetsSidebarTab template and improve UI structure
- Extract sidebar template into reusable AssetSidebarTemplate component
  - Replace PrimeVue Tabs with TextButton for better visual consistency
  - Add i18n key for "Back to all assets" button
  - Improve job detail view header layout with better spacing
  - Maintain existing functionality while cleaning up template structure
2025-10-22 22:58:40 +09:00
Jin Yi
4ca3e5a5c7 refactor: replace custom formatExecutionTime with formatDuration utility
- Use existing formatDuration util from shared utils instead of custom
  implementation
  - Extract execution time formatting to computed property for cleaner
  template
  - Remove 'any' type assertion in MediaAssetActions, use proper types
2025-10-22 22:58:32 +09:00
Jin Yi
dc12899041 feat: add job folder view for grouped batch outputs in media assets
- Add folder view to display all outputs from a single batch job
  - Show output count badge on assets with multiple batch outputs
  - Add job ID display and copy functionality in folder view header
  - Display execution time for batch jobs
  - Implement download functionality for output assets only
  - Add inspect action to asset more menu
  - Extract prompt ID from asset IDs using new UUID utility
  - Add comprehensive tests for UUID extraction utilities
2025-10-22 22:58:24 +09:00
Jin Yi
e94f7e9d90 refactor: useQueueStore 2025-10-22 22:58:13 +09:00
Jin Yi
e9515af00c fix: test code 2025-10-22 22:57:52 +09:00
Jin Yi
cd96b5147d feat: Add includePublic parameter to getAssetsByTag API
- Add optional includePublic parameter (defaults to true) to getAssetsByTag
- Exclude public assets for media assets in sidebar by passing false
- Use URLSearchParams for cleaner query string construction

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:57:42 +09:00
Jin Yi
198bacc762 feat: Improve media asset display with file format tags and filename truncation
- Add file format tags (PNG, JPG, etc.) for input directory assets
- Truncate long filenames in input assets with originalFilename preservation
- Show file format chip independently from duration chip
- Fix conditional display logic for chips in MediaAssetCard
- Apply consistent filename truncation (20 chars) across cloud assets

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:57:30 +09:00
Jin Yi
219aa2e913 chore: unexpected export 2025-10-22 22:57:20 +09:00
Jin Yi
0b020cfad3 refactor: Apply PR #6112 review feedback for Media Assets feature
- Move composables to platform/assets directory structure
- Extract interface-based abstraction (IAssetsProvider) for cloud/internal implementations
- Move constants to module scope to avoid re-initialization
- Extract helper functions (truncateFilename, assetMappers) for reusability
- Rename getMediaTypeFromFilename to return singular form (image/video/audio)
- Add deprecated plural version for backward compatibility
- Add comprehensive test coverage for new utility functions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:56:58 +09:00
Jin Yi
2bb54650b4 feat: Add Media Assets sidebar tab for file management
- Implement new sidebar tab for managing imported/generated files
- Add separate composables for internal and cloud environments
- Display execution time from history API on generated outputs
- Support gallery view with keyboard navigation
- Auto-truncate long filenames in cloud environment
- Add utility functions for media type detection
- Enable feature only in development mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:56:50 +09:00
213 changed files with 3028 additions and 2769 deletions

View File

@@ -1,23 +1,23 @@
<template>
<div
class="task-div relative grid min-h-52 max-w-48"
class="task-div max-w-48 min-h-52 grid relative"
:class="{ 'opacity-75': isLoading }"
>
<Card
class="relative h-full max-w-48 overflow-hidden"
class="max-w-48 relative h-full overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
<i
v-if="runner.state === 'error'"
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
style="font-size: 10rem"
/>
<img
v-if="task.headerImg"
:src="task.headerImg"
class="h-full w-full object-contain px-4 pt-4 opacity-25"
class="object-contain w-full h-full opacity-25 pt-4 px-4"
/>
</template>
<template #title>
@@ -27,7 +27,7 @@
{{ description }}
</template>
<template #footer>
<div class="mt-1 flex gap-4">
<div class="flex gap-4 mt-1">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
@@ -73,7 +73,7 @@ defineEmits<{
// Bindings
const description = computed(() =>
runner.value.state === 'error'
? (props.task.errorDescription ?? props.task.shortDescription)
? props.task.errorDescription ?? props.task.shortDescription
: props.task.shortDescription
)

View File

@@ -285,33 +285,6 @@ export class ComfyPage {
} = {}) {
await this.goto()
// Mock remote config endpoint for cloud builds
// Cloud builds (rh-test) call /api/features on startup, which blocks initialization
// if the endpoint doesn't exist or times out. Try real backend first, fallback to empty config.
await this.page.route('**/api/features', async (route) => {
try {
// Try to get response from real backend
const response = await route.fetch()
if (response.ok()) {
await route.fulfill({ response })
} else {
// Backend doesn't have endpoint, return empty config
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({})
})
}
} catch {
// Network error, return empty config
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({})
})
}
})
// Mock release endpoint to prevent changelog popups
if (mockReleases) {
await this.page.route('**/releases**', async (route) => {

View File

@@ -7,9 +7,7 @@ test.describe('Bottom Panel Shortcuts', () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.skip('should toggle shortcuts panel visibility', async ({
comfyPage
}) => {
test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
// Initially shortcuts panel should be hidden
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
@@ -30,9 +28,7 @@ test.describe('Bottom Panel Shortcuts', () => {
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
})
test.skip('should display essentials shortcuts tab', async ({
comfyPage
}) => {
test('should display essentials shortcuts tab', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
@@ -66,9 +62,7 @@ test.describe('Bottom Panel Shortcuts', () => {
).toBeVisible()
})
test.skip('should display view controls shortcuts tab', async ({
comfyPage
}) => {
test('should display view controls shortcuts tab', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
@@ -94,7 +88,7 @@ test.describe('Bottom Panel Shortcuts', () => {
).toBeVisible()
})
test.skip('should switch between shortcuts tabs', async ({ comfyPage }) => {
test('should switch between shortcuts tabs', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
@@ -128,9 +122,7 @@ test.describe('Bottom Panel Shortcuts', () => {
).not.toHaveAttribute('aria-selected', 'true')
})
test.skip('should display formatted keyboard shortcuts', async ({
comfyPage
}) => {
test('should display formatted keyboard shortcuts', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
@@ -152,7 +144,7 @@ test.describe('Bottom Panel Shortcuts', () => {
expect(hasModifiers).toBeTruthy()
})
test.skip('should maintain panel state when switching to terminal', async ({
test('should maintain panel state when switching to terminal', async ({
comfyPage
}) => {
// Open shortcuts panel first
@@ -180,7 +172,7 @@ test.describe('Bottom Panel Shortcuts', () => {
).toBeVisible()
})
test.skip('should handle keyboard navigation', async ({ comfyPage }) => {
test('should handle keyboard navigation', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
@@ -206,7 +198,7 @@ test.describe('Bottom Panel Shortcuts', () => {
).toHaveAttribute('aria-selected', 'true')
})
test.skip('should close panel by clicking shortcuts button again', async ({
test('should close panel by clicking shortcuts button again', async ({
comfyPage
}) => {
// Open shortcuts panel
@@ -224,7 +216,7 @@ test.describe('Bottom Panel Shortcuts', () => {
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
})
test.skip('should display shortcuts in organized columns', async ({
test('should display shortcuts in organized columns', async ({
comfyPage
}) => {
// Open shortcuts panel
@@ -259,7 +251,7 @@ test.describe('Bottom Panel Shortcuts', () => {
).toHaveAttribute('aria-selected', 'true')
})
test.skip('should open settings dialog when clicking manage shortcuts button', async ({
test('should open settings dialog when clicking manage shortcuts button', async ({
comfyPage
}) => {
// Open shortcuts panel

View File

@@ -65,7 +65,7 @@ test.describe('Change Tracker', () => {
})
})
test.skip('Can group multiple change actions into a single transaction', async ({
test('Can group multiple change actions into a single transaction', async ({
comfyPage
}) => {
const node = (await comfyPage.getFirstNodeRef())!
@@ -153,7 +153,7 @@ test.describe('Change Tracker', () => {
await expect(node).toBeCollapsed()
})
test.skip('Can detect changes in workflow.extra', async ({ comfyPage }) => {
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'

View File

@@ -152,7 +152,7 @@ const customColorPalettes: Record<string, Palette> = {
}
test.describe('Color Palette', () => {
test.skip('Can show custom color palette', async ({ comfyPage }) => {
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
// doesn't update the store immediately.
@@ -174,7 +174,7 @@ test.describe('Color Palette', () => {
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
})
test.skip('Can add custom color palette', async ({ comfyPage }) => {
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
@@ -199,7 +199,7 @@ test.describe('Node Color Adjustments', () => {
await comfyPage.loadWorkflow('nodes/every_node_color')
})
test.skip('should adjust opacity via node opacity setting', async ({
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
@@ -217,7 +217,7 @@ test.describe('Node Color Adjustments', () => {
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
test.skip('should persist color adjustments when changing themes', async ({
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
@@ -245,7 +245,7 @@ test.describe('Node Color Adjustments', () => {
}
})
test.skip('should lighten node colors when switching to light theme', async ({
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
@@ -261,7 +261,7 @@ test.describe('Node Color Adjustments', () => {
await node?.clickContextMenuOption('Colors')
})
test.skip('should persist color adjustments when changing custom node colors', async ({
test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
await comfyPage.page
@@ -272,7 +272,7 @@ test.describe('Node Color Adjustments', () => {
)
})
test.skip('should persist color adjustments when removing custom node color', async ({
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
await comfyPage.page

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Copy Paste', () => {
test.skip('Can copy and paste node', async ({ comfyPage }) => {
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
@@ -15,7 +15,7 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
test.skip('Can copy and paste node with link', async ({ comfyPage }) => {
test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
@@ -35,7 +35,7 @@ test.describe('Copy Paste', () => {
expect(resultString).toBe(originalString + originalString)
})
test.skip('Can copy and paste widget value', async ({ comfyPage }) => {
test('Can copy and paste widget value', async ({ comfyPage }) => {
// Copy width value (512) from empty latent node to KSampler's seed.
// KSampler's seed
await comfyPage.canvas.click({
@@ -60,7 +60,7 @@ test.describe('Copy Paste', () => {
/**
* https://github.com/Comfy-Org/ComfyUI_frontend/issues/98
*/
test.skip('Paste in text area with node previously copied', async ({
test('Paste in text area with node previously copied', async ({
comfyPage
}) => {
await comfyPage.clickEmptyLatentNode()
@@ -77,7 +77,7 @@ test.describe('Copy Paste', () => {
)
})
test.skip('Copy text area does not copy node', async ({ comfyPage }) => {
test('Copy text area does not copy node', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.inputValue()
@@ -89,7 +89,7 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})
test.skip('Copy node by dragging + alt', async ({ comfyPage }) => {
test('Copy node by dragging + alt', async ({ comfyPage }) => {
// TextEncodeNode1
await comfyPage.page.mouse.move(618, 191)
await comfyPage.page.keyboard.down('Alt')

View File

@@ -27,7 +27,7 @@ test.describe('Custom Icons', () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.skip('sidebar tab icons use custom SVGs', async ({ comfyPage }) => {
test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => {
// Find the icon in the sidebar
const icon = comfyPage.page.locator(
'.icon-\\[comfy--ai-model\\].side-bar-button-icon'

View File

@@ -35,7 +35,7 @@ test.describe('Load workflow warning', () => {
})
})
test.skip('Does not report warning on undo/redo', async ({ comfyPage }) => {
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.loadWorkflow('missing/missing_nodes')
@@ -301,9 +301,7 @@ test.describe('Settings', () => {
})
test.describe('Support', () => {
test('Should open external zendesk link with OSS tag', async ({
comfyPage
}) => {
test('Should open external zendesk link', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
@@ -311,10 +309,6 @@ test.describe('Support', () => {
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
const url = new URL(newPage.url())
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
await newPage.close()
})
})

View File

@@ -29,9 +29,7 @@ test.describe('DOM Widget', () => {
await expect(lastMultiline).not.toBeVisible()
})
test.skip('Position update when entering focus mode', async ({
comfyPage
}) => {
test('Position update when entering focus mode', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Execution', () => {
test.skip('Report error on unconnected slot', async ({ comfyPage }) => {
test('Report error on unconnected slot', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()

View File

@@ -15,7 +15,7 @@ test.describe('Graph Canvas Menu', () => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
test.skip('Can toggle link visibility', async ({ comfyPage }) => {
test('Can toggle link visibility', async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
@@ -39,7 +39,7 @@ test.describe('Graph Canvas Menu', () => {
)
})
test.skip('Focus mode button is clickable and has correct test id', async ({
test('Focus mode button is clickable and has correct test id', async ({
comfyPage
}) => {
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
@@ -51,7 +51,7 @@ test.describe('Graph Canvas Menu', () => {
await comfyPage.nextFrame()
})
test.skip('Zoom controls popup opens and closes', async ({ comfyPage }) => {
test('Zoom controls popup opens and closes', async ({ comfyPage }) => {
// Find the zoom button by its percentage text content
const zoomButton = comfyPage.page.locator('button').filter({
hasText: '%'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -22,11 +22,11 @@ test.describe('Group Node', () => {
await libraryTab.open()
})
test.skip('Is added to node library sidebar', async ({ comfyPage }) => {
test('Is added to node library sidebar', async ({ comfyPage }) => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})
test.skip('Can be added to canvas using node library sidebar', async ({
test('Can be added to canvas using node library sidebar', async ({
comfyPage
}) => {
const initialNodeCount = await comfyPage.getGraphNodesCount()
@@ -39,7 +39,7 @@ test.describe('Group Node', () => {
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
})
test.skip('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
@@ -66,7 +66,7 @@ test.describe('Group Node', () => {
).toHaveLength(0)
})
test.skip('Displays preview on bookmark hover', async ({ comfyPage }) => {
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
@@ -261,14 +261,14 @@ test.describe('Group Node', () => {
await groupNode.copy()
})
test.skip('Copies and pastes group node within the same workflow', async ({
test('Copies and pastes group node within the same workflow', async ({
comfyPage
}) => {
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 2)
})
test.skip('Copies and pastes group node after clearing workflow', async ({
test('Copies and pastes group node after clearing workflow', async ({
comfyPage
}) => {
// Set setting
@@ -281,7 +281,7 @@ test.describe('Group Node', () => {
await verifyNodeLoaded(comfyPage, 1)
})
test.skip('Copies and pastes group node into a newly created blank workflow', async ({
test('Copies and pastes group node into a newly created blank workflow', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
@@ -289,7 +289,7 @@ test.describe('Group Node', () => {
await verifyNodeLoaded(comfyPage, 1)
})
test.skip('Copies and pastes group node across different workflows', async ({
test('Copies and pastes group node across different workflows', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
@@ -297,7 +297,7 @@ test.describe('Group Node', () => {
await verifyNodeLoaded(comfyPage, 1)
})
test.skip('Serializes group node after copy and paste across workflows', async ({
test('Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])

View File

@@ -38,7 +38,7 @@ test.describe('History API v2', () => {
expect(historyItem.prompt.extra_data).toHaveProperty('client_id')
})
test.skip('Can load workflow from history using history_v2 endpoint', async ({
test('Can load workflow from history using history_v2 endpoint', async ({
comfyPage
}) => {
// Simple mock workflow for testing

View File

@@ -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 }) => {
@@ -14,7 +14,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Item Interaction', () => {
test.skip('Can select/delete all items', async ({ comfyPage }) => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
@@ -22,9 +22,7 @@ test.describe('Item Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
})
test.skip('Can pin/unpin items with keyboard shortcut', async ({
comfyPage
}) => {
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await comfyPage.canvas.press('KeyP')
@@ -62,7 +60,7 @@ test.describe('Node Interaction', () => {
})
})
test.skip('@2x Can highlight selected', async ({ comfyPage }) => {
test('@2x Can highlight selected', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
@@ -152,7 +150,7 @@ test.describe('Node Interaction', () => {
})
})
test.skip('Can drag node', async ({ comfyPage }) => {
test('Can drag node', async ({ comfyPage }) => {
await comfyPage.dragNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
})
@@ -165,7 +163,7 @@ test.describe('Node Interaction', () => {
// Test both directions of edge connection.
;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => {
test.skip(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
@@ -180,7 +178,7 @@ test.describe('Node Interaction', () => {
})
})
test.skip('Can move link', async ({ comfyPage }) => {
test('Can move link', async ({ comfyPage }) => {
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace
@@ -211,7 +209,7 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
})
test.skip('Auto snap&highlight when dragging link over node', async ({
test('Auto snap&highlight when dragging link over node', async ({
comfyPage,
comfyMouse
}) => {
@@ -224,12 +222,12 @@ test.describe('Node Interaction', () => {
})
})
test.skip('Can adjust widget value', async ({ comfyPage }) => {
test('Can adjust widget value', async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png')
})
test.skip('Link snap to slot', async ({ comfyPage }) => {
test('Link snap to slot', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/snap_to_slot')
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
@@ -246,9 +244,7 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
})
test.skip('Can batch move links by drag with shift', async ({
comfyPage
}) => {
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
@@ -270,7 +266,7 @@ test.describe('Node Interaction', () => {
)
})
test.skip('Can batch disconnect links with ctrl+alt+click', async ({
test('Can batch disconnect links with ctrl+alt+click', async ({
comfyPage
}) => {
const loadCheckpointClipSlotPos = {
@@ -287,7 +283,7 @@ test.describe('Node Interaction', () => {
)
})
test.skip('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
test('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -300,7 +296,7 @@ test.describe('Node Interaction', () => {
)
})
test.skip('Can close prompt dialog with canvas click (number widget)', async ({
test('Can close prompt dialog with canvas click (number widget)', async ({
comfyPage
}) => {
const numberWidgetPos = {
@@ -322,7 +318,7 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
})
test.skip('Can close prompt dialog with canvas click (text widget)', async ({
test('Can close prompt dialog with canvas click (text widget)', async ({
comfyPage
}) => {
const textWidgetPos = {
@@ -348,7 +344,7 @@ test.describe('Node Interaction', () => {
)
})
test.skip('Can double click node title to edit', async ({ comfyPage }) => {
test('Can double click node title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
@@ -376,7 +372,7 @@ test.describe('Node Interaction', () => {
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
})
test.skip('Can group selected nodes', async ({ comfyPage }) => {
test('Can group selected nodes', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
@@ -389,7 +385,7 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
})
test.skip('Can fit group to contents', async ({ comfyPage }) => {
test('Can fit group to contents', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
@@ -398,7 +394,7 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
})
test.skip('Can pin/unpin nodes', async ({ comfyPage }) => {
test('Can pin/unpin nodes', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.nextFrame()
@@ -408,7 +404,7 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
})
test.skip('Can bypass/unbypass nodes with keyboard shortcut', async ({
test('Can bypass/unbypass nodes with keyboard shortcut', async ({
comfyPage
}) => {
await comfyPage.select2Nodes()
@@ -422,7 +418,7 @@ test.describe('Node Interaction', () => {
})
test.describe('Group Interaction', () => {
test.skip('Can double click group title to edit', async ({ comfyPage }) => {
test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.canvas.dblclick({
position: {
@@ -438,21 +434,21 @@ test.describe('Group Interaction', () => {
})
test.describe('Canvas Interaction', () => {
test.skip('Can zoom in/out', async ({ comfyPage }) => {
test('Can zoom in/out', async ({ comfyPage }) => {
await comfyPage.zoom(-100)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
await comfyPage.zoom(200)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
})
test.skip('Can zoom very far out', async ({ comfyPage }) => {
test('Can zoom very far out', async ({ comfyPage }) => {
await comfyPage.zoom(100, 12)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png')
await comfyPage.zoom(-100, 12)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png')
})
test.skip('Can zoom in/out with ctrl+shift+vertical-drag', async ({
test('Can zoom in/out with ctrl+shift+vertical-drag', async ({
comfyPage
}) => {
await comfyPage.page.keyboard.down('Control')
@@ -469,7 +465,7 @@ test.describe('Canvas Interaction', () => {
await comfyPage.page.keyboard.up('Shift')
})
test.skip('Can zoom in/out after decreasing canvas zoom speed setting', async ({
test('Can zoom in/out after decreasing canvas zoom speed setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
@@ -484,7 +480,7 @@ test.describe('Canvas Interaction', () => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
})
test.skip('Can zoom in/out after increasing canvas zoom speed', async ({
test('Can zoom in/out after increasing canvas zoom speed', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
@@ -499,12 +495,12 @@ test.describe('Canvas Interaction', () => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
})
test.skip('Can pan', async ({ comfyPage }) => {
test('Can pan', async ({ comfyPage }) => {
await comfyPage.pan({ x: 200, y: 200 })
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
})
test.skip('Cursor style changes when panning', async ({ comfyPage }) => {
test('Cursor style changes when panning', async ({ comfyPage }) => {
const getCursorStyle = async () => {
return await comfyPage.page.evaluate(() => {
return (
@@ -534,7 +530,7 @@ test.describe('Canvas Interaction', () => {
})
// https://github.com/Comfy-Org/litegraph.js/pull/424
test.skip('Properly resets dragging state after pan mode sequence', async ({
test('Properly resets dragging state after pan mode sequence', async ({
comfyPage
}) => {
const getCursorStyle = async () => {
@@ -570,10 +566,7 @@ test.describe('Canvas Interaction', () => {
expect(await getCursorStyle()).toBe('default')
})
test.skip('Can pan when dragging a link', async ({
comfyPage,
comfyMouse
}) => {
test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
await comfyMouse.move(posSlot1)
const posEmpty = comfyPage.emptySpace
@@ -593,7 +586,7 @@ test.describe('Canvas Interaction', () => {
await comfyMouse.drop()
})
test.skip('Can pan very far and back', async ({ comfyPage }) => {
test('Can pan very far and back', async ({ comfyPage }) => {
// intentionally slice the edge of where the clip text encode dom widgets are
await comfyPage.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png')
@@ -609,7 +602,7 @@ test.describe('Canvas Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
})
test.skip('@mobile Can pan with touch', async ({ comfyPage }) => {
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.closeMenu()
await comfyPage.panWithTouch({ x: 200, y: 200 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
@@ -643,19 +636,19 @@ test.describe('Widget Interaction', () => {
})
test.describe('Load workflow', () => {
test.skip('Can load workflow with string node id', async ({ comfyPage }) => {
test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
})
test.skip('Can load workflow with ("STRING",) input node', async ({
test('Can load workflow with ("STRING",) input node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('inputs/string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
})
test.skip('Restore workflow on reload (switch workflow)', async ({
test('Restore workflow on reload (switch workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
@@ -664,7 +657,7 @@ test.describe('Load workflow', () => {
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
test.skip('Restore workflow on reload (modify workflow)', async ({
test('Restore workflow on reload (modify workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
@@ -721,9 +714,7 @@ test.describe('Load workflow', () => {
expect(activeWorkflowName).toEqual(workflowB)
})
test.skip('Restores sidebar workflows after reload', async ({
comfyPage
}) => {
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
@@ -746,7 +737,7 @@ test.describe('Load workflow', () => {
})
})
test.skip('Auto fit view after loading workflow', async ({ comfyPage }) => {
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
@@ -758,7 +749,7 @@ test.describe('Load duplicate workflow', () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.skip('A workflow can be loaded multiple times in a row', async ({
test('A workflow can be loaded multiple times in a row', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
@@ -847,7 +838,7 @@ test.describe('Canvas Navigation', () => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')
})
test.skip('Left-click drag in empty area should pan canvas', async ({
test('Left-click drag in empty area should pan canvas', async ({
comfyPage
}) => {
await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 })
@@ -856,7 +847,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Middle-click drag should pan canvas', async ({ comfyPage }) => {
test('Middle-click drag should pan canvas', async ({ comfyPage }) => {
await comfyPage.page.mouse.move(50, 50)
await comfyPage.page.mouse.down({ button: 'middle' })
await comfyPage.page.mouse.move(150, 150)
@@ -867,7 +858,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Mouse wheel should zoom in/out', async ({ comfyPage }) => {
test('Mouse wheel should zoom in/out', async ({ comfyPage }) => {
await comfyPage.page.mouse.move(400, 300)
await comfyPage.page.mouse.wheel(0, -120)
await comfyPage.nextFrame()
@@ -882,9 +873,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Left-click on node should not pan canvas', async ({
comfyPage
}) => {
test('Left-click on node should not pan canvas', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
const selectedCount = await comfyPage.getSelectedGraphNodesCount()
expect(selectedCount).toBe(1)
@@ -899,7 +888,7 @@ test.describe('Canvas Navigation', () => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'standard')
})
test.skip('Left-click drag in empty area should select nodes', async ({
test('Left-click drag in empty area should select nodes', async ({
comfyPage
}) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
@@ -925,7 +914,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Middle-click drag should pan canvas', async ({ comfyPage }) => {
test('Middle-click drag should pan canvas', async ({ comfyPage }) => {
await comfyPage.page.mouse.move(50, 50)
await comfyPage.page.mouse.down({ button: 'middle' })
await comfyPage.page.mouse.move(150, 150)
@@ -936,9 +925,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Ctrl + mouse wheel should zoom in/out', async ({
comfyPage
}) => {
test('Ctrl + mouse wheel should zoom in/out', async ({ comfyPage }) => {
await comfyPage.page.mouse.move(400, 300)
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.mouse.wheel(0, -120)
@@ -957,7 +944,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Left-click on node should select node (not start selection box)', async ({
test('Left-click on node should select node (not start selection box)', async ({
comfyPage
}) => {
await comfyPage.clickTextEncodeNode1()
@@ -968,9 +955,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Space + left-click drag should pan canvas', async ({
comfyPage
}) => {
test('Space + left-click drag should pan canvas', async ({ comfyPage }) => {
// Click canvas to focus it
await comfyPage.page.click('canvas')
await comfyPage.nextFrame()
@@ -983,7 +968,7 @@ test.describe('Canvas Navigation', () => {
)
})
test.skip('Space key overrides default left-click behavior', async ({
test('Space key overrides default left-click behavior', async ({
comfyPage
}) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
@@ -1029,7 +1014,7 @@ test.describe('Canvas Navigation', () => {
})
})
test.skip('Shift + mouse wheel should pan canvas horizontally', async ({
test('Shift + mouse wheel should pan canvas horizontally', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning')
@@ -1067,7 +1052,7 @@ test.describe('Canvas Navigation', () => {
})
test.describe('Edge Cases', () => {
test.skip('Multiple modifier keys work correctly in legacy mode', async ({
test('Multiple modifier keys work correctly in legacy mode', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')

View File

@@ -27,9 +27,7 @@ test.describe('Canvas Event', () => {
// See https://github.com/microsoft/playwright/issues/31580
})
test.skip('Emit litegraph:canvas empty-double-click', async ({
comfyPage
}) => {
test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
const doubleClickPromise = comfyPage.doubleClickCanvas()
const event = await eventPromise

View File

@@ -25,7 +25,7 @@ test.describe('Load Workflow in Media', () => {
// 'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test.skip(`Load workflow in ${fileName} (drop from filesystem)`, async ({
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
@@ -37,7 +37,7 @@ test.describe('Load Workflow in Media', () => {
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
]
urls.forEach(async (url) => {
test.skip(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('LOD Threshold', () => {
test.skip('Should switch to low quality mode at correct zoom threshold', async ({
test('Should switch to low quality mode at correct zoom threshold', async ({
comfyPage
}) => {
// Load a workflow with some nodes to render
@@ -81,7 +81,7 @@ test.describe('LOD Threshold', () => {
expect(zoomedInState.lowQuality).toBe(false)
})
test.skip('Should update threshold when font size setting changes', async ({
test('Should update threshold when font size setting changes', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
@@ -122,7 +122,7 @@ test.describe('LOD Threshold', () => {
expect(afterZoom.lowQuality).toBe(true)
})
test.skip('Should disable LOD when font size is set to 0', async ({
test('Should disable LOD when font size is set to 0', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
@@ -149,7 +149,7 @@ test.describe('LOD Threshold', () => {
expect(state.scale).toBeLessThan(0.2) // Very zoomed out
})
test.skip('Should show visual difference between LOD on and off', async ({
test('Should show visual difference between LOD on and off', async ({
comfyPage
}) => {
// Load a workflow with text-heavy nodes for clear visual difference

View File

@@ -7,7 +7,7 @@ test.describe('Menu', () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.skip('Can register sidebar tab', async ({ comfyPage }) => {
test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)

View File

@@ -9,7 +9,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Node Badge', () => {
test.skip('Can add badge', async ({ comfyPage }) => {
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
@@ -26,7 +26,7 @@ test.describe('Node Badge', () => {
await expect(comfyPage.canvas).toHaveScreenshot('node-badge.png')
})
test.skip('Can add multiple badges', async ({ comfyPage }) => {
test('Can add multiple badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
@@ -46,7 +46,7 @@ test.describe('Node Badge', () => {
await expect(comfyPage.canvas).toHaveScreenshot('node-badge-multiple.png')
})
test.skip('Can add badge left-side', async ({ comfyPage }) => {
test('Can add badge left-side', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
@@ -68,7 +68,7 @@ test.describe('Node Badge', () => {
test.describe('Node source badge', () => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test.skip(`Shows node badges (${mode})`, async ({ comfyPage }) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
@@ -81,7 +81,7 @@ test.describe('Node source badge', () => {
})
test.describe('Node badge color', () => {
test.skip('Can show node badge with unknown color palette', async ({
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
@@ -97,7 +97,7 @@ test.describe('Node badge color', () => {
)
})
test.skip('Can show node badge with light color palette', async ({
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -9,27 +9,27 @@ test.beforeEach(async ({ comfyPage }) => {
// If an input is optional by node definition, it should be shown as
// a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', () => {
test.skip('No shape specified', async ({ comfyPage }) => {
test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test.skip('Wrong shape specified', async ({ comfyPage }) => {
test('Wrong shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_wrong_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test.skip('Correct shape specified', async ({ comfyPage }) => {
test('Correct shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_correct_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test.skip('Force input', async ({ comfyPage }) => {
test('Force input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/force_input')
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
})
test.skip('Default input', async ({ comfyPage }) => {
test('Default input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/default_input')
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
})
@@ -65,18 +65,18 @@ test.describe('Optional input', () => {
const renamedInput = inputs.find((w) => w.name === 'breadth')
expect(renamedInput).toBeUndefined()
})
test.skip('slider', async ({ comfyPage }) => {
test('slider', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
})
test.skip('unknown converted widget', async ({ comfyPage }) => {
test('unknown converted widget', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
await comfyPage.loadWorkflow('missing/missing_nodes_converted_widget')
await expect(comfyPage.canvas).toHaveScreenshot(
'missing_nodes_converted_widget.png'
)
})
test.skip('dynamically added input', async ({ comfyPage }) => {
test('dynamically added input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/dynamically_added_input')
await expect(comfyPage.canvas).toHaveScreenshot(
'dynamically_added_input.png'

View File

@@ -27,9 +27,7 @@ test.describe('Node Help', () => {
})
test.describe('Selection Toolbox', () => {
test.skip('Should open help menu for selected node', async ({
comfyPage
}) => {
test('Should open help menu for selected node', async ({ comfyPage }) => {
// Load a workflow with a node
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('default')
@@ -66,9 +64,7 @@ test.describe('Node Help', () => {
})
test.describe('Node Library Sidebar', () => {
test.skip('Should open help menu from node library', async ({
comfyPage
}) => {
test('Should open help menu from node library', async ({ comfyPage }) => {
// Open the node library sidebar
await comfyPage.menu.nodeLibraryTab.open()
@@ -101,7 +97,7 @@ test.describe('Node Help', () => {
await expect(helpPage.locator('.node-help-content')).toBeVisible()
})
test.skip('Should show node library tab when clicking back from help page', async ({
test('Should show node library tab when clicking back from help page', async ({
comfyPage
}) => {
// Open the node library sidebar
@@ -149,7 +145,7 @@ test.describe('Node Help', () => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test.skip('Should display loading state while fetching help', async ({
test('Should display loading state while fetching help', async ({
comfyPage
}) => {
// Mock slow network response
@@ -180,7 +176,7 @@ test.describe('Node Help', () => {
await expect(helpPage).toContainText('Test Help Content')
})
test.skip('Should display fallback content when help file not found', async ({
test('Should display fallback content when help file not found', async ({
comfyPage
}) => {
// Mock 404 response for help files
@@ -209,7 +205,7 @@ test.describe('Node Help', () => {
await expect(helpPage).toContainText('Outputs')
})
test.skip('Should render markdown with images correctly', async ({
test('Should render markdown with images correctly', async ({
comfyPage
}) => {
// Mock response with markdown containing images
@@ -255,7 +251,7 @@ test.describe('Node Help', () => {
)
})
test.skip('Should render video elements with source tags in markdown', async ({
test('Should render video elements with source tags in markdown', async ({
comfyPage
}) => {
// Mock response with video elements
@@ -316,7 +312,7 @@ test.describe('Node Help', () => {
)
})
test.skip('Should handle custom node documentation paths', async ({
test('Should handle custom node documentation paths', async ({
comfyPage
}) => {
// First load workflow with custom node
@@ -369,9 +365,7 @@ This is documentation for a custom node.
}
})
test.skip('Should sanitize dangerous HTML content', async ({
comfyPage
}) => {
test('Should sanitize dangerous HTML content', async ({ comfyPage }) => {
// Mock response with potentially dangerous content
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
@@ -430,7 +424,7 @@ This is documentation for a custom node.
await expect(helpPage.locator('img[alt="Safe Image"]')).toBeVisible()
})
test.skip('Should handle locale-specific documentation', async ({
test('Should handle locale-specific documentation', async ({
comfyPage
}) => {
// Mock different responses for different locales
@@ -474,9 +468,7 @@ This is English documentation.
await comfyPage.setSetting('Comfy.Locale', 'en')
})
test.skip('Should handle network errors gracefully', async ({
comfyPage
}) => {
test('Should handle network errors gracefully', async ({ comfyPage }) => {
// Mock network error
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await route.abort('failed')
@@ -502,7 +494,7 @@ This is English documentation.
expect(content).toBeTruthy()
})
test.skip('Should update help content when switching between nodes', async ({
test('Should update help content when switching between nodes', async ({
comfyPage
}) => {
// Mock different help content for different nodes

View File

@@ -14,9 +14,7 @@ test.describe('Node search box', () => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test.skip(`Can trigger on empty canvas double click`, async ({
comfyPage
}) => {
test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
})
@@ -48,14 +46,14 @@ test.describe('Node search box', () => {
await expect(comfyPage.searchBox.input).toBeVisible()
})
test.skip('Can add node', async ({ comfyPage }) => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
})
test.skip('Can auto link node', async ({ comfyPage }) => {
test('Can auto link node', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
@@ -64,7 +62,7 @@ test.describe('Node search box', () => {
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
})
test.skip('Can auto link batch moved node', async ({ comfyPage }) => {
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
const outputSlot1Pos = {
@@ -88,7 +86,7 @@ test.describe('Node search box', () => {
)
})
test.skip('Link release connecting to node with no slots', async ({
test('Link release connecting to node with no slots', async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
@@ -100,9 +98,7 @@ test.describe('Node search box', () => {
)
})
test.skip('Has correct aria-labels on search results', async ({
comfyPage
}) => {
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint'
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.input.waitFor({ state: 'visible' })
@@ -154,7 +150,7 @@ test.describe('Node search box', () => {
await comfyPage.doubleClickCanvas()
})
test.skip('Can add filter', async ({ comfyPage }) => {
test('Can add filter', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await expectFilterChips(comfyPage, ['MODEL'])
})
@@ -201,13 +197,13 @@ test.describe('Node search box', () => {
await expect(comfyPage.searchBox.input).toBeVisible()
})
test.skip('Can add multiple filters', async ({ comfyPage }) => {
test('Can add multiple filters', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.addFilter('CLIP', 'Output Type')
await expectFilterChips(comfyPage, ['MODEL', 'CLIP'])
})
test.skip('Can remove filter', async ({ comfyPage }) => {
test('Can remove filter', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.removeFilter(0)
await expectFilterChips(comfyPage, [])
@@ -220,7 +216,7 @@ test.describe('Node search box', () => {
await comfyPage.searchBox.addFilter('utils', 'Category')
})
test.skip('Can remove first filter', async ({ comfyPage }) => {
test('Can remove first filter', async ({ comfyPage }) => {
await comfyPage.searchBox.removeFilter(0)
await expectFilterChips(comfyPage, ['CLIP', 'utils'])
await comfyPage.searchBox.removeFilter(0)
@@ -229,12 +225,12 @@ test.describe('Node search box', () => {
await expectFilterChips(comfyPage, [])
})
test.skip('Can remove middle filter', async ({ comfyPage }) => {
test('Can remove middle filter', async ({ comfyPage }) => {
await comfyPage.searchBox.removeFilter(1)
await expectFilterChips(comfyPage, ['MODEL', 'utils'])
})
test.skip('Can remove last filter', async ({ comfyPage }) => {
test('Can remove last filter', async ({ comfyPage }) => {
await comfyPage.searchBox.removeFilter(2)
await expectFilterChips(comfyPage, ['MODEL', 'CLIP'])
})
@@ -246,14 +242,12 @@ test.describe('Node search box', () => {
await comfyPage.doubleClickCanvas()
})
test.skip('focuses input after adding a filter', async ({ comfyPage }) => {
test('focuses input after adding a filter', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await expect(comfyPage.searchBox.input).toHaveFocus()
})
test.skip('focuses input after removing a filter', async ({
comfyPage
}) => {
test('focuses input after removing a filter', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.removeFilter(0)
await expect(comfyPage.searchBox.input).toHaveFocus()
@@ -268,7 +262,7 @@ test.describe('Release context menu', () => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test.skip('Can trigger on link release', async ({ comfyPage }) => {
test('Can trigger on link release', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
@@ -277,7 +271,7 @@ test.describe('Release context menu', () => {
)
})
test.skip('Can search and add node from context menu', async ({
test('Can search and add node from context menu', async ({
comfyPage,
comfyMouse
}) => {

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Note Node', () => {
test.skip('Can load node nodes', async ({ comfyPage }) => {
test('Can load node nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/note_nodes')
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
})

View File

@@ -8,14 +8,14 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Primitive Node', () => {
test.skip('Can load with correct size', async ({ comfyPage }) => {
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
})
// When link is dropped on widget, it should automatically convert the widget
// to input.
test.skip('Can connect to widget', async ({ comfyPage }) => {
test('Can connect to widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
@@ -26,7 +26,7 @@ test.describe('Primitive Node', () => {
)
})
test.skip('Can connect to dom widget', async ({ comfyPage }) => {
test('Can connect to dom widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget'
)
@@ -38,7 +38,7 @@ test.describe('Primitive Node', () => {
)
})
test.skip('Can connect to static primitive node', async ({ comfyPage }) => {
test('Can connect to static primitive node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -7,7 +7,7 @@ test.describe('Release Notifications', () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.skip('should show help center with release information', async ({
test('should show help center with release information', async ({
comfyPage
}) => {
// Mock release API with test data instead of empty array
@@ -63,7 +63,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).not.toBeVisible()
})
test.skip('should not show release notifications when mocked (default behavior)', async ({
test('should not show release notifications when mocked (default behavior)', async ({
comfyPage
}) => {
// Use default setup (mockReleases: true)
@@ -94,9 +94,7 @@ test.describe('Release Notifications', () => {
).not.toBeVisible()
})
test.skip('should handle release API errors gracefully', async ({
comfyPage
}) => {
test('should handle release API errors gracefully', async ({ comfyPage }) => {
// Mock API to return an error
await comfyPage.page.route('**/releases**', async (route) => {
const url = route.request().url()
@@ -133,7 +131,7 @@ test.describe('Release Notifications', () => {
).toBeVisible()
})
test.skip('should hide "What\'s New" section when notifications are disabled', async ({
test('should hide "What\'s New" section when notifications are disabled', async ({
comfyPage
}) => {
// Disable version update notifications
@@ -221,7 +219,7 @@ test.describe('Release Notifications', () => {
expect(apiCallCount).toBe(0)
})
test.skip('should show "What\'s New" section when notifications are enabled', async ({
test('should show "What\'s New" section when notifications are enabled', async ({
comfyPage
}) => {
// Enable version update notifications (default behavior)
@@ -274,7 +272,7 @@ test.describe('Release Notifications', () => {
).toBeVisible()
})
test.skip('should toggle "What\'s New" section when setting changes', async ({
test('should toggle "What\'s New" section when setting changes', async ({
comfyPage
}) => {
// Mock release API with test data
@@ -329,7 +327,7 @@ test.describe('Release Notifications', () => {
await expect(whatsNewSection).not.toBeVisible()
})
test.skip('should handle edge case with empty releases and disabled notifications', async ({
test('should handle edge case with empty releases and disabled notifications', async ({
comfyPage
}) => {
// Disable notifications

View File

@@ -77,7 +77,7 @@ test.describe('Remote COMBO Widget', () => {
await comfyPage.page.unroute('**/api/models/checkpoints**')
})
test.skip('lazy loads options when widget is added from node library', async ({
test('lazy loads options when widget is added from node library', async ({
comfyPage
}) => {
const nodeName = 'Remote Widget Node'
@@ -104,9 +104,7 @@ test.describe('Remote COMBO Widget', () => {
expect(widgetOptions).toEqual(mockOptions)
})
test.skip('applies query parameters from input spec', async ({
comfyPage
}) => {
test('applies query parameters from input spec', async ({ comfyPage }) => {
const nodeName = 'Remote Widget Node With Sort Query Param'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
@@ -115,7 +113,7 @@ test.describe('Remote COMBO Widget', () => {
expect(widgetOptions).toEqual([...mockOptions].sort())
})
test.skip('handles empty list of options', async ({ comfyPage }) => {
test('handles empty list of options', async ({ comfyPage }) => {
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
@@ -130,7 +128,7 @@ test.describe('Remote COMBO Widget', () => {
expect(widgetOptions).toEqual([])
})
test.skip('falls back to default value when non-200 response', async ({
test('falls back to default value when non-200 response', async ({
comfyPage
}) => {
await comfyPage.page.route(
@@ -167,7 +165,7 @@ test.describe('Remote COMBO Widget', () => {
expect(requestWasMade).toBe(false)
})
test.skip('fetches options immediately after widget is added to graph', async ({
test('fetches options immediately after widget is added to graph', async ({
comfyPage
}) => {
const requestPromise = comfyPage.page.waitForRequest((request) =>
@@ -180,7 +178,7 @@ test.describe('Remote COMBO Widget', () => {
})
test.describe('Refresh Behavior', () => {
test.skip('refresh button is visible in selection toolbar when node is selected', async ({
test('refresh button is visible in selection toolbar when node is selected', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
@@ -199,7 +197,7 @@ test.describe('Remote COMBO Widget', () => {
).toBeVisible()
})
test.skip('refreshes options when TTL expires', async ({ comfyPage }) => {
test('refreshes options when TTL expires', async ({ comfyPage }) => {
// Fulfill each request with a unique timestamp
await comfyPage.page.route(
'**/api/models/checkpoints**',
@@ -230,7 +228,7 @@ test.describe('Remote COMBO Widget', () => {
expect(refreshedOptions).not.toEqual(initialOptions)
})
test.skip('does not refresh when TTL is not set', async ({ comfyPage }) => {
test('does not refresh when TTL is not set', async ({ comfyPage }) => {
let requestCount = 0
await comfyPage.page.route(
'**/api/models/checkpoints**',
@@ -253,7 +251,7 @@ test.describe('Remote COMBO Widget', () => {
expect(requestCount).toBe(1) // Should only make initial request
})
test.skip('retries failed requests with backoff', async ({ comfyPage }) => {
test('retries failed requests with backoff', async ({ comfyPage }) => {
const timestamps: number[] = []
await comfyPage.page.route(
'**/api/models/checkpoints**',
@@ -280,9 +278,7 @@ test.describe('Remote COMBO Widget', () => {
expect(intervals[1]).toBeGreaterThan(intervals[0])
})
test.skip('clicking refresh button forces a refresh', async ({
comfyPage
}) => {
test('clicking refresh button forces a refresh', async ({ comfyPage }) => {
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
@@ -308,7 +304,7 @@ test.describe('Remote COMBO Widget', () => {
expect(refreshedOptions).not.toEqual(initialOptions)
})
test.skip('control_after_refresh is applied after refresh', async ({
test('control_after_refresh is applied after refresh', async ({
comfyPage
}) => {
const options = [
@@ -344,7 +340,7 @@ test.describe('Remote COMBO Widget', () => {
})
test.describe('Cache Behavior', () => {
test.skip('reuses cached data between widgets with same params', async ({
test('reuses cached data between widgets with same params', async ({
comfyPage
}) => {
let requestCount = 0

View File

@@ -12,7 +12,7 @@ test.describe('Reroute Node', () => {
await comfyPage.setupWorkflowsDirectory({})
})
test.skip('loads from inserted workflow', async ({ comfyPage }) => {
test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node.json'
await comfyPage.setupWorkflowsDirectory({
[workflowName]: 'links/single_connected_reroute_node.json'
@@ -44,12 +44,12 @@ test.describe('LiteGraph Native Reroute Node', () => {
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test.skip('loads from workflow', async ({ comfyPage }) => {
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
})
test.skip('@2x @0.5x Can add reroute by alt clicking on link', async ({
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
comfyPage
}) => {
const loadCheckpointNode = (
@@ -75,7 +75,7 @@ test.describe('LiteGraph Native Reroute Node', () => {
)
})
test.skip('Can add reroute by clicking middle of link context menu', async ({
test('Can add reroute by clicking middle of link context menu', async ({
comfyPage
}) => {
const loadCheckpointNode = (
@@ -102,7 +102,7 @@ test.describe('LiteGraph Native Reroute Node', () => {
)
})
test.skip('Can delete link that is connected to two reroutes', async ({
test('Can delete link that is connected to two reroutes', async ({
comfyPage
}) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Canvas Right Click Menu', () => {
test.skip('Can add node', async ({ comfyPage }) => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click()
@@ -20,7 +20,7 @@ test.describe('Canvas Right Click Menu', () => {
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
})
test.skip('Can add group', async ({ comfyPage }) => {
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click()
@@ -28,7 +28,7 @@ test.describe('Canvas Right Click Menu', () => {
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
})
test.skip('Can convert to group node', async ({ comfyPage }) => {
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
@@ -44,7 +44,7 @@ test.describe('Canvas Right Click Menu', () => {
})
test.describe('Node Right Click Menu', () => {
test.skip('Can open properties panel', async ({ comfyPage }) => {
test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Properties Panel').click()
@@ -54,7 +54,7 @@ test.describe('Node Right Click Menu', () => {
)
})
test.skip('Can collapse', async ({ comfyPage }) => {
test('Can collapse', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Collapse').click()
@@ -64,7 +64,7 @@ test.describe('Node Right Click Menu', () => {
)
})
test.skip('Can collapse (Node Badge)', async ({ comfyPage }) => {
test('Can collapse (Node Badge)', async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
@@ -82,7 +82,7 @@ test.describe('Node Right Click Menu', () => {
)
})
test.skip('Can bypass', async ({ comfyPage }) => {
test('Can bypass', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Bypass').click()
@@ -92,7 +92,7 @@ test.describe('Node Right Click Menu', () => {
)
})
test.skip('Can pin and unpin', async ({ comfyPage }) => {
test('Can pin and unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
@@ -111,7 +111,7 @@ test.describe('Node Right Click Menu', () => {
)
})
test.skip('Can move after unpin', async ({ comfyPage }) => {
test('Can move after unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
@@ -125,7 +125,7 @@ test.describe('Node Right Click Menu', () => {
)
})
test.skip('Can pin/unpin selected nodes', async ({ comfyPage }) => {
test('Can pin/unpin selected nodes', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.rightClickEmptyLatentNode()

View File

@@ -15,7 +15,7 @@ test.describe('Selection Toolbox', () => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test.skip('shows selection toolbox', async ({ comfyPage }) => {
test('shows selection toolbox', async ({ comfyPage }) => {
// By default, selection toolbox should be enabled
await expect(comfyPage.selectionToolbox).not.toBeVisible()
@@ -30,7 +30,7 @@ test.describe('Selection Toolbox', () => {
)
})
test.skip('shows at correct position when node is pasted', async ({
test('shows at correct position when node is pasted', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
@@ -66,9 +66,7 @@ test.describe('Selection Toolbox', () => {
await expect(comfyPage.selectionToolbox).not.toBeVisible()
})
test.skip('shows border only with multiple selections', async ({
comfyPage
}) => {
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])
@@ -96,7 +94,7 @@ test.describe('Selection Toolbox', () => {
)
})
test.skip('displays bypass button in toolbox when nodes are selected', async ({
test('displays bypass button in toolbox when nodes are selected', async ({
comfyPage
}) => {
// A group + a KSampler node

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -72,7 +72,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
throw new Error('Could not open More Options menu - popover not showing')
}
test.skip('opens Node Info from More Options menu', async ({ comfyPage }) => {
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
await openMoreOptions(comfyPage)
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
@@ -82,7 +82,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
await comfyPage.nextFrame()
})
test.skip('changes node shape via Shape submenu', async ({ comfyPage }) => {
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialShape = await nodeRef.getProperty<number>('shape')
@@ -99,9 +99,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
expect(newShape).toBe(1)
})
test.skip('changes node color via Color submenu swatch', async ({
comfyPage
}) => {
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialColor = await nodeRef.getProperty<string | undefined>('color')
@@ -119,7 +117,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
}
})
test.skip('renames a node using Rename action', async ({ comfyPage }) => {
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await openMoreOptions(comfyPage)
await comfyPage.page
@@ -136,7 +134,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
expect(newTitle).toBe('RenamedNode')
})
test.skip('closes More Options menu when clicking outside', async ({
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
@@ -153,7 +151,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
).not.toBeVisible()
})
test.skip('closes More Options menu when clicking the button again (toggle)', async ({
test('closes More Options menu when clicking the button again (toggle)', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)

View File

@@ -12,7 +12,7 @@ test.describe('Node library sidebar', () => {
await tab.open()
})
test.skip('Node preview and drag to canvas', async ({ comfyPage }) => {
test('Node preview and drag to canvas', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('sampling').click()
@@ -49,7 +49,7 @@ test.describe('Node library sidebar', () => {
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
})
test.skip('Bookmark node', async ({ comfyPage }) => {
test('Bookmark node', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('sampling').click()
@@ -68,7 +68,7 @@ test.describe('Node library sidebar', () => {
expect(await comfyPage.page.isVisible('.node-lib-node-preview')).toBe(true)
})
test.skip('Ignores unrecognized node', async ({ comfyPage }) => {
test('Ignores unrecognized node', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo'])
const tab = comfyPage.menu.nodeLibraryTab
@@ -76,13 +76,13 @@ test.describe('Node library sidebar', () => {
expect(await tab.getNode('foo').count()).toBe(0)
})
test.skip('Displays empty bookmarks folder', async ({ comfyPage }) => {
test('Displays empty bookmarks folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
expect(await tab.getFolder('foo').count()).toBe(1)
})
test.skip('Can add new bookmark folder', async ({ comfyPage }) => {
test('Can add new bookmark folder', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.newFolderButton.click()
const textInput = comfyPage.page.locator('.editable-text input')
@@ -95,7 +95,7 @@ test.describe('Node library sidebar', () => {
).toEqual(['New Folder/'])
})
test.skip('Can add nested bookmark folder', async ({ comfyPage }) => {
test('Can add nested bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
@@ -112,7 +112,7 @@ test.describe('Node library sidebar', () => {
).toEqual(['foo/', 'foo/bar/'])
})
test.skip('Can delete bookmark folder', async ({ comfyPage }) => {
test('Can delete bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
@@ -124,7 +124,7 @@ test.describe('Node library sidebar', () => {
).toEqual([])
})
test.skip('Can rename bookmark folder', async ({ comfyPage }) => {
test('Can rename bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
@@ -140,7 +140,7 @@ test.describe('Node library sidebar', () => {
).toEqual(['bar/'])
})
test.skip('Can add bookmark by dragging node to bookmark folder', async ({
test('Can add bookmark by dragging node to bookmark folder', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
@@ -155,7 +155,7 @@ test.describe('Node library sidebar', () => {
).toEqual(['foo/', 'foo/KSamplerAdvanced'])
})
test.skip('Can add bookmark by clicking bookmark button', async ({
test('Can add bookmark by clicking bookmark button', async ({
comfyPage
}) => {
const tab = comfyPage.menu.nodeLibraryTab
@@ -166,9 +166,7 @@ test.describe('Node library sidebar', () => {
).toEqual(['KSamplerAdvanced'])
})
test.skip('Can unbookmark node (Top level bookmark)', async ({
comfyPage
}) => {
test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'KSamplerAdvanced'
])
@@ -179,9 +177,7 @@ test.describe('Node library sidebar', () => {
).toEqual([])
})
test.skip('Can unbookmark node (Library node bookmark)', async ({
comfyPage
}) => {
test('Can unbookmark node (Library node bookmark)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'KSamplerAdvanced'
])
@@ -196,7 +192,7 @@ test.describe('Node library sidebar', () => {
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
})
test.skip('Can customize icon', async ({ comfyPage }) => {
test('Can customize icon', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
@@ -219,7 +215,7 @@ test.describe('Node library sidebar', () => {
})
})
// If color is left as default, it should not be saved
test.skip('Can customize icon (default field)', async ({ comfyPage }) => {
test('Can customize icon (default field)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
@@ -238,7 +234,7 @@ test.describe('Node library sidebar', () => {
})
})
test.skip('Can customize bookmark color after interacting with color options', async ({
test('Can customize bookmark color after interacting with color options', async ({
comfyPage
}) => {
// Open customization dialog
@@ -278,7 +274,7 @@ test.describe('Node library sidebar', () => {
await expect(setting['foo/'].color).not.toBe('')
})
test.skip('Can rename customized bookmark folder', async ({ comfyPage }) => {
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
'foo/': {
@@ -307,7 +303,7 @@ test.describe('Node library sidebar', () => {
})
})
test.skip('Can delete customized bookmark folder', async ({ comfyPage }) => {
test('Can delete customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
'foo/': {
@@ -327,7 +323,7 @@ test.describe('Node library sidebar', () => {
).toEqual({})
})
test.skip('Can filter nodes in both trees', async ({ comfyPage }) => {
test('Can filter nodes in both trees', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/',
'foo/KSamplerAdvanced',

View File

@@ -16,7 +16,7 @@ test.describe('Workflows sidebar', () => {
await comfyPage.setupWorkflowsDirectory({})
})
test.skip('Can create new blank workflow', async ({ comfyPage }) => {
test('Can create new blank workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
@@ -29,7 +29,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can show top level saved workflows', async ({ comfyPage }) => {
test('Can show top level saved workflows', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json',
'workflow2.json': 'default.json'
@@ -42,7 +42,7 @@ test.describe('Workflows sidebar', () => {
)
})
test.skip('Can duplicate workflow', async ({ comfyPage }) => {
test('Can duplicate workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
@@ -72,7 +72,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can open workflow after insert', async ({ comfyPage }) => {
test('Can open workflow after insert', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'nodes/single_ksampler.json'
})
@@ -91,7 +91,7 @@ test.describe('Workflows sidebar', () => {
expect((await comfyPage.getNodes()).length).toEqual(1)
})
test.skip('Can rename nested workflow from opened workflow item', async ({
test('Can rename nested workflow from opened workflow item', async ({
comfyPage
}) => {
await comfyPage.setupWorkflowsDirectory({
@@ -117,7 +117,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can save workflow as', async ({ comfyPage }) => {
test('Can save workflow as', async ({ comfyPage }) => {
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
@@ -133,7 +133,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Exported workflow does not contain localized slot names', async ({
test('Exported workflow does not contain localized slot names', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
@@ -153,7 +153,7 @@ test.describe('Workflows sidebar', () => {
}
})
test.skip('Can export same workflow with different locales', async ({
test('Can export same workflow with different locales', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
@@ -185,7 +185,7 @@ test.describe('Workflows sidebar', () => {
expect(downloadedContent).toEqual(downloadedContentZh)
})
test.skip('Can save workflow as with same name', async ({ comfyPage }) => {
test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
await comfyPage.nextFrame()
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
@@ -200,7 +200,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can save temporary workflow with unmodified name', async ({
test('Can save temporary workflow with unmodified name', async ({
comfyPage
}) => {
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
@@ -214,9 +214,7 @@ test.describe('Workflows sidebar', () => {
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
})
test.skip('Can overwrite other workflows with save as', async ({
comfyPage
}) => {
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
const topbar = comfyPage.menu.topbar
await topbar.saveWorkflow('workflow1.json')
await topbar.saveWorkflowAs('workflow2.json')
@@ -242,7 +240,7 @@ test.describe('Workflows sidebar', () => {
)
})
test.skip('Does not report warning when switching between opened workflows', async ({
test('Does not report warning when switching between opened workflows', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes')
@@ -260,7 +258,7 @@ test.describe('Workflows sidebar', () => {
).not.toBeVisible()
})
test.skip('Can close saved-workflows from the open workflows section', async ({
test('Can close saved-workflows from the open workflows section', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.saveWorkflow(
@@ -275,7 +273,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can close saved workflow with command', async ({ comfyPage }) => {
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.executeCommand('Workspace.CloseWorkflow')
@@ -284,9 +282,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can delete workflows (confirm disabled)', async ({
comfyPage
}) => {
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
const { topbar, workflowsTab } = comfyPage.menu
@@ -305,7 +301,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can delete workflows', async ({ comfyPage }) => {
test('Can delete workflows', async ({ comfyPage }) => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18.json'
@@ -323,9 +319,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can duplicate workflow from context menu', async ({
comfyPage
}) => {
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
@@ -344,9 +338,7 @@ test.describe('Workflows sidebar', () => {
])
})
test.skip('Can drop workflow from workflows sidebar', async ({
comfyPage
}) => {
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})

View File

@@ -468,9 +468,7 @@ test.describe('Subgraph Operations', () => {
expect(finalNodeCount).toBe(initialNodeCount + 1)
})
test.skip('Can undo and redo operations in subgraph', async ({
comfyPage
}) => {
test('Can undo and redo operations in subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
@@ -685,7 +683,7 @@ test.describe('Subgraph Operations', () => {
expect(widgetCount).toBe(0)
})
test.skip('Multiple promoted widgets are handled correctly', async ({
test('Multiple promoted widgets are handled correctly', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(

View File

@@ -69,7 +69,7 @@ test.describe('Templates', () => {
}
})
test.skip('Can load template workflows', async ({ comfyPage }) => {
test('Can load template workflows', async ({ comfyPage }) => {
// Clear the workflow
await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')

View File

@@ -12,9 +12,7 @@ test.describe('Vue Node Groups', () => {
await comfyPage.vueNodes.waitForNodes()
})
test.skip('should allow creating groups with hotkey', async ({
comfyPage
}) => {
test('should allow creating groups with hotkey', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
@@ -24,7 +22,7 @@ test.describe('Vue Node Groups', () => {
)
})
test.skip('should allow fitting group to contents', async ({ comfyPage }) => {
test('should allow fitting group to contents', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -9,7 +9,7 @@ test.describe('Vue Nodes Canvas Pan', () => {
await comfyPage.vueNodes.waitForNodes()
})
test.skip('@mobile Can pan with touch', async ({ comfyPage }) => {
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -9,7 +9,7 @@ test.describe('Vue Nodes Zoom', () => {
await comfyPage.vueNodes.waitForNodes()
})
test.skip('should not capture drag while zooming with ctrl+shift+drag', async ({
test('should not capture drag while zooming with ctrl+shift+drag', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -109,7 +109,7 @@ test.describe('Vue Node Link Interaction', () => {
await fitToViewInstant(comfyPage)
})
test.skip('should show a link dragging out from a slot when dragging on a slot', async ({
test('should show a link dragging out from a slot when dragging on a slot', async ({
comfyPage,
comfyMouse
}) => {
@@ -218,7 +218,7 @@ test.describe('Vue Node Link Interaction', () => {
expect(await samplerInput.getLinkCount()).toBe(0)
})
test.skip('should reuse the existing origin when dragging an input link', async ({
test('should reuse the existing origin when dragging an input link', async ({
comfyPage,
comfyMouse
}) => {
@@ -255,7 +255,7 @@ test.describe('Vue Node Link Interaction', () => {
await comfyMouse.drop()
})
test.skip('ctrl+alt drag from an input starts a fresh link', async ({
test('ctrl+alt drag from an input starts a fresh link', async ({
comfyPage,
comfyMouse
}) => {
@@ -395,7 +395,7 @@ test.describe('Vue Node Link Interaction', () => {
expect(await vaeInput.getLinkCount()).toBe(1)
})
test.skip('rerouted input drag preview remains anchored to reroute', async ({
test('rerouted input drag preview remains anchored to reroute', async ({
comfyPage,
comfyMouse
}) => {
@@ -480,7 +480,7 @@ test.describe('Vue Node Link Interaction', () => {
expect(linkDetails?.parentId).not.toBeNull()
})
test.skip('rerouted output shift-drag preview remains anchored to reroute', async ({
test('rerouted output shift-drag preview remains anchored to reroute', async ({
comfyPage,
comfyMouse
}) => {
@@ -639,7 +639,7 @@ test.describe('Vue Node Link Interaction', () => {
})
})
test.skip('shift-dragging an output with multiple links should drag all links', async ({
test('shift-dragging an output with multiple links should drag all links', async ({
comfyPage,
comfyMouse
}) => {
@@ -694,7 +694,7 @@ test.describe('Vue Node Link Interaction', () => {
}
})
test.skip('should snap to node center while dragging and link on drop', async ({
test('should snap to node center while dragging and link on drop', async ({
comfyPage,
comfyMouse
}) => {
@@ -743,7 +743,7 @@ test.describe('Vue Node Link Interaction', () => {
expect(linked?.targetId).toBe(samplerNode.id)
})
test.skip('should snap to a specific compatible slot when targeting it', async ({
test('should snap to a specific compatible slot when targeting it', async ({
comfyPage,
comfyMouse
}) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,8 +1,8 @@
import {
type ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import type { Position } from '../../../../fixtures/types'
test.describe('Vue Node Moving', () => {
@@ -29,7 +29,7 @@ test.describe('Vue Node Moving', () => {
expect(diffY).toBeGreaterThan(0)
}
test.skip('should allow moving nodes by dragging', async ({ comfyPage }) => {
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
@@ -42,7 +42,7 @@ test.describe('Vue Node Moving', () => {
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
})
test.skip('@mobile should allow moving nodes by dragging on touch devices', async ({
test('@mobile should allow moving nodes by dragging on touch devices', async ({
comfyPage
}) => {
// Disable minimap (gets in way of the node on small screens)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -11,7 +11,7 @@ test.describe('Vue Node Custom Colors', () => {
await comfyPage.vueNodes.waitForNodes()
})
test.skip('displays color picker button and allows color selection', async ({
test('displays color picker button and allows color selection', async ({
comfyPage
}) => {
const loadCheckpointNode = comfyPage.page.locator('[data-node-id]').filter({
@@ -30,14 +30,14 @@ test.describe('Vue Node Custom Colors', () => {
)
})
test.skip('should load node colors from workflow', async ({ comfyPage }) => {
test('should load node colors from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-dark-all-colors.png'
)
})
test.skip('should show brightened node colors on light theme', async ({
test('should show brightened node colors on light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -13,9 +13,7 @@ test.describe('Vue Nodes - LOD', () => {
await comfyPage.loadWorkflow('default')
})
test.skip('should toggle LOD based on zoom threshold', async ({
comfyPage
}) => {
test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {
await comfyPage.vueNodes.waitForNodes()
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -9,7 +9,7 @@ test.describe('Vue Upload Widgets', () => {
await comfyPage.vueNodes.waitForNodes()
})
test.skip('should hide canvas-only upload buttons', async ({ comfyPage }) => {
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Combo text widget', () => {
test.skip('Truncates text when resized', async ({ comfyPage }) => {
test('Truncates text when resized', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
await expect(comfyPage.canvas).toHaveScreenshot(
'load-checkpoint-resized-min-width.png'
@@ -19,16 +19,14 @@ test.describe('Combo text widget', () => {
)
})
test.skip("Doesn't truncate when space still available", async ({
comfyPage
}) => {
test("Doesn't truncate when space still available", async ({ comfyPage }) => {
await comfyPage.resizeEmptyLatentNode(0.8, 0.8)
await expect(comfyPage.canvas).toHaveScreenshot(
'empty-latent-resized-80-percent.png'
)
})
test.skip('Can revert to full text', async ({ comfyPage }) => {
test('Can revert to full text', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.8, 1, true)
await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png')
})
@@ -82,7 +80,7 @@ test.describe('Combo text widget', () => {
})
test.describe('Boolean widget', () => {
test.skip('Can toggle', async ({ comfyPage }) => {
test('Can toggle', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
const node = (await comfyPage.getFirstNodeRef())!
@@ -95,7 +93,7 @@ test.describe('Boolean widget', () => {
})
test.describe('Slider widget', () => {
test.skip('Can drag adjust value', async ({ comfyPage }) => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
await comfyPage.page.waitForTimeout(300)
const node = (await comfyPage.getFirstNodeRef())!
@@ -117,7 +115,7 @@ test.describe('Slider widget', () => {
})
test.describe('Number widget', () => {
test.skip('Can drag adjust value', async ({ comfyPage }) => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/seed_widget')
await comfyPage.page.waitForTimeout(300)
@@ -139,7 +137,7 @@ test.describe('Number widget', () => {
})
test.describe('Dynamic widget manipulation', () => {
test.skip('Auto expand node when widget is added dynamically', async ({
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
@@ -155,12 +153,12 @@ test.describe('Dynamic widget manipulation', () => {
})
test.describe('Image widget', () => {
test.skip('Can load image', async ({ comfyPage }) => {
test('Can load image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png')
})
test.skip('Can drag and drop image', async ({ comfyPage }) => {
test('Can drag and drop image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
// Get position of the load image node
@@ -184,7 +182,7 @@ test.describe('Image widget', () => {
expect(filename).toBe('image32x32.webp')
})
test.skip('Can change image by changing the filename combo value', async ({
test('Can change image by changing the filename combo value', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
@@ -322,7 +320,7 @@ test.describe('Animated image widget', () => {
})
test.describe('Load audio widget', () => {
test.skip('Can load audio', async ({ comfyPage }) => {
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' })

View File

@@ -1,7 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import { importX } from 'eslint-plugin-import-x'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
@@ -24,17 +23,10 @@ const commonGlobals = {
} as const
const settings = {
'import-x/resolver-next': [
createTypeScriptImportResolver({
alwaysTryTypes: true,
project: [
'./tsconfig.json',
'./apps/*/tsconfig.json',
'./packages/*/tsconfig.json'
],
noWarnOnMultipleProjects: true
})
],
'import/resolver': {
typescript: true,
node: true
},
tailwindcss: {
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
functions: ['cn', 'clsx', 'tw']
@@ -76,7 +68,9 @@ export default defineConfig([
projectService: {
allowDefaultProject: [
'vite.electron.config.mts',
'vite.types.config.mts'
'vite.types.config.mts',
'playwright.config.ts',
'playwright.i18n.config.ts'
]
}
}
@@ -254,17 +248,5 @@ export default defineConfig([
rules: {
'no-console': 'off'
}
},
{
files: ['scripts/**/*.js'],
languageOptions: {
globals: {
...globals.node
}
},
rules: {
'@typescript-eslint/no-floating-promises': 'off',
'no-console': 'off'
}
}
])

15
global.d.ts vendored
View File

@@ -4,19 +4,12 @@ declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
declare const __USE_PROD_CONFIG__: boolean
declare const __MIXPANEL_TOKEN__: string
interface Window {
__CONFIG__: {
mixpanel_token?: string
subscription_required?: boolean
server_health_alert?: {
message: string
tooltip?: string
severity?: 'info' | 'warning' | 'error'
badge?: string
}
}
type BuildFeatureFlags = {
REQUIRE_SUBSCRIPTION: boolean
}
declare const __BUILD_FLAGS__: BuildFeatureFlags
interface Navigator {
/**

View File

@@ -1065,7 +1065,7 @@ audio.comfy-audio.empty-audio-widget {
}
.isLOD .lg-node-header {
border-radius: 0;
border-radius: 0px;
pointer-events: none;
}

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 9.99996L11.9427 7.94263C11.6926 7.69267 11.3536 7.55225 11 7.55225C10.6464 7.55225 10.3074 7.69267 10.0573 7.94263L9 9M8 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.51377 12.671L4.77612 14.3921C4.67222 14.6346 4.32853 14.6346 4.22463 14.3921L3.48699 12.671C3.45664 12.6002 3.40022 12.5437 3.32942 12.5134L1.60825 11.7757C1.36581 11.6718 1.36581 11.3282 1.60825 11.2243L3.32942 10.4866C3.40022 10.4563 3.45664 10.3998 3.48699 10.329L4.22463 8.60787C4.32853 8.36544 4.67222 8.36544 4.77612 8.60787L5.51377 10.329C5.54411 10.3998 5.60053 10.4563 5.67134 10.4866L7.39251 11.2243C7.63494 11.3282 7.63494 11.6718 7.39251 11.7757L5.67134 12.5134C5.60053 12.5437 5.54411 12.6002 5.51377 12.671Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M5 5H5.0001" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -474,3 +474,93 @@ export function formatDuration(milliseconds: number): string {
return parts.join(' ')
}
// Module scope constants to avoid re-initialization on every call
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi']
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac']
const THREE_D_EXTENSIONS = ['obj', 'fbx', 'gltf', 'glb']
/**
* Truncates a filename while preserving the extension
* @param filename The filename to truncate
* @param maxLength Maximum length for the filename without extension
* @returns Truncated filename with extension preserved
*/
export function truncateFilename(
filename: string,
maxLength: number = 20
): string {
if (!filename || filename.length <= maxLength) {
return filename
}
const lastDotIndex = filename.lastIndexOf('.')
const nameWithoutExt =
lastDotIndex > -1 ? filename.substring(0, lastDotIndex) : filename
const extension = lastDotIndex > -1 ? filename.substring(lastDotIndex) : ''
// If the name without extension is short enough, return as is
if (nameWithoutExt.length <= maxLength) {
return filename
}
// Calculate how to split the truncation
const halfLength = Math.floor((maxLength - 3) / 2) // -3 for '...'
const start = nameWithoutExt.substring(0, halfLength)
const end = nameWithoutExt.substring(nameWithoutExt.length - halfLength)
return `${start}...${end}${extension}`
}
/**
* Determines the media type from a filename's extension (singular form)
* @param filename The filename to analyze
* @returns The media type: 'image', 'video', 'audio', or '3D'
*/
export function getMediaTypeFromFilename(
filename: string
): 'image' | 'video' | 'audio' | '3D' {
if (!filename) return 'image'
const ext = filename.split('.').pop()?.toLowerCase()
if (!ext) return 'image'
if (IMAGE_EXTENSIONS.includes(ext)) return 'image'
if (VIDEO_EXTENSIONS.includes(ext)) return 'video'
if (AUDIO_EXTENSIONS.includes(ext)) return 'audio'
if (THREE_D_EXTENSIONS.includes(ext)) return '3D'
return 'image'
}
/**
* @deprecated Use getMediaTypeFromFilename instead - returns plural form for legacy compatibility
* @param filename The filename to analyze
* @returns The media type in plural form: 'images', 'videos', 'audios', '3D'
*/
export function getMediaTypeFromFilenamePlural(filename: string): string {
const type = getMediaTypeFromFilename(filename)
switch (type) {
case 'image':
return 'images'
case 'video':
return 'videos'
case 'audio':
return 'audios'
case '3D':
return '3D'
default:
return 'images'
}
}
/**
* @deprecated Use getMediaTypeFromFilename instead - kept for backward compatibility
* @param filename The filename to analyze
* @returns The media kind: 'image', 'video', 'audio', or '3D'
*/
export function getMediaKindFromFilename(
filename: string
): 'image' | 'video' | 'audio' | '3D' {
return getMediaTypeFromFilename(filename)
}

64
pnpm-lock.yaml generated
View File

@@ -183,12 +183,18 @@ catalogs:
lint-staged:
specifier: ^15.2.7
version: 15.2.7
markdown-table:
specifier: ^3.0.4
version: 3.0.4
mixpanel-browser:
specifier: ^2.71.0
version: 2.71.0
nx:
specifier: 21.4.1
version: 21.4.1
picocolors:
specifier: ^1.1.1
version: 1.1.1
pinia:
specifier: ^2.1.7
version: 2.2.2
@@ -196,14 +202,20 @@ catalogs:
specifier: ^1.8.0
version: 1.8.0
prettier:
specifier: ^3.3.2
specifier: ^3.6.2
version: 3.6.2
pretty-bytes:
specifier: ^7.1.0
version: 7.1.0
primeicons:
specifier: ^7.0.0
version: 7.0.0
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
@@ -585,18 +597,30 @@ importers:
lint-staged:
specifier: 'catalog:'
version: 15.2.7
markdown-table:
specifier: 'catalog:'
version: 3.0.4
mixpanel-browser:
specifier: 'catalog:'
version: 2.71.0
nx:
specifier: 'catalog:'
version: 21.4.1
picocolors:
specifier: 'catalog:'
version: 1.1.1
postcss-html:
specifier: 'catalog:'
version: 1.8.0
prettier:
specifier: 'catalog:'
version: 3.6.2
pretty-bytes:
specifier: 'catalog:'
version: 7.1.0
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.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
@@ -746,11 +770,11 @@ importers:
packages/shared-frontend-utils:
dependencies:
axios:
specifier: ^1.11.0
specifier: 'catalog:'
version: 1.11.0
devDependencies:
typescript:
specifier: ^5.9.2
specifier: 'catalog:'
version: 5.9.2
packages/tailwind-utils:
@@ -6388,6 +6412,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
pretty-bytes@7.1.0:
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
engines: {node: '>=20'}
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -6690,6 +6718,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'}
@@ -6844,6 +6885,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'}
@@ -14326,6 +14371,8 @@ snapshots:
prettier@3.6.2: {}
pretty-bytes@7.1.0: {}
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
@@ -14773,6 +14820,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
@@ -14964,6 +15020,8 @@ snapshots:
source-map@0.6.1: {}
source-map@0.7.6: {}
speakingurl@14.0.1: {}
sprintf-js@1.0.3: {}

View File

@@ -1,64 +1,29 @@
/**
* Utility functions for downloading files
*/
import { isCloud } from '@/platform/distribution/types'
// Constants
const DEFAULT_DOWNLOAD_FILENAME = 'download.png'
/**
* Trigger a download by creating a temporary anchor element
* @param href - The URL or blob URL to download
* @param filename - The filename to suggest to the browser
*/
function triggerLinkDownload(href: string, filename: string): void {
const link = document.createElement('a')
link.href = href
link.download = filename
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* Download a file from a URL by creating a temporary anchor element
* @param url - The URL of the file to download (must be a valid URL string)
* @param filename - Optional filename override (will use URL filename or default if not provided)
* @throws {Error} If the URL is invalid or empty
*/
export function downloadFile(url: string, filename?: string): void {
export const downloadFile = (url: string, filename?: string): void => {
if (!url || typeof url !== 'string' || url.trim().length === 0) {
throw new Error('Invalid URL provided for download')
}
const inferredFilename =
const link = document.createElement('a')
link.href = url
link.download =
filename || extractFilenameFromUrl(url) || DEFAULT_DOWNLOAD_FILENAME
if (isCloud) {
// Assets from cross-origin (e.g., GCS) cannot be downloaded this way
void downloadViaBlobFetch(url, inferredFilename).catch((error) => {
console.error('Failed to download file', error)
})
return
}
triggerLinkDownload(url, inferredFilename)
}
/**
* Download a Blob by creating a temporary object URL and anchor element
* @param filename - The filename to suggest to the browser
* @param blob - The Blob to download
*/
export function downloadBlob(filename: string, blob: Blob): void {
const url = URL.createObjectURL(blob)
triggerLinkDownload(url, filename)
// Revoke on the next microtask to give the browser time to start the download
queueMicrotask(() => URL.revokeObjectURL(url))
// Trigger download
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
@@ -74,15 +39,3 @@ const extractFilenameFromUrl = (url: string): string | null => {
return null
}
}
const downloadViaBlobFetch = async (
href: string,
filename: string
): Promise<void> => {
const response = await fetch(href)
if (!response.ok) {
throw new Error(`Failed to fetch ${href}: ${response.status}`)
}
const blob = await response.blob()
downloadBlob(filename, blob)
}

View File

@@ -74,9 +74,7 @@ const activeSidebarTabId = computed(
)
const sidebarStateKey = computed(() => {
return unifiedWidth.value
? 'unified-sidebar'
: (activeSidebarTabId.value ?? '')
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
})
</script>

View File

@@ -2,6 +2,6 @@ import { defineAsyncComponent } from 'vue'
import { isCloud } from '@/platform/distribution/types'
export default isCloud && window.__CONFIG__?.subscription_required
export default isCloud && __BUILD_FLAGS__.REQUIRE_SUBSCRIPTION
? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))
: defineAsyncComponent(() => import('./ComfyQueueButton.vue'))

View File

@@ -54,7 +54,7 @@ const {
}>()
const topStyle = computed(() => {
const baseClasses = 'relative p-0'
const baseClasses = 'relative p-0 overflow-hidden'
const ratioClasses = {
square: 'aspect-square',

View File

@@ -27,6 +27,28 @@
<PasswordFields />
<!-- Personal Data Consent Checkbox -->
<FormField
v-slot="$field"
name="personalDataConsent"
class="flex items-center gap-2"
>
<Checkbox
input-id="comfy-org-sign-up-personal-data-consent"
:binary="true"
:invalid="$field.invalid"
/>
<label
for="comfy-org-sign-up-personal-data-consent"
class="text-base font-medium opacity-80"
>
{{ t('auth.signup.personalDataConsentLabel') }}
</label>
<small v-if="$field.error" class="-mt-4 text-red-500">{{
$field.error.message
}}</small>
</FormField>
<!-- Auth Error Message -->
<Message v-if="authError" severity="error">
{{ authError }}
@@ -46,6 +68,7 @@ import type { FormSubmitEvent } from '@primevue/forms'
import { Form, FormField } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import { useI18n } from 'vue-i18n'

View File

@@ -2,22 +2,7 @@
<!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
<div
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
>
<!-- Native drag area for Electron -->
<div
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div class="flex h-full items-center">
<WorkflowTabs />
<TopbarBadges />
</div>
</div>
</template>
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
<SideToolbar />
</template>
@@ -38,6 +23,7 @@
/>
</template>
</LiteGraphCanvasSplitterOverlay>
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
id="graph-canvas"
ref="canvasRef"
@@ -105,8 +91,6 @@ import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
@@ -145,7 +129,6 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
const emit = defineEmits<{
ready: []
@@ -180,10 +163,6 @@ const selectionToolboxEnabled = computed(() =>
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
const showUI = computed(
() => !workspaceStore.focusMode && betaMenuEnabled.value
)
// Feature flags
const { shouldRenderVueNodes } = useVueFeatureFlags()

View File

@@ -75,7 +75,7 @@ let promptInput = findPromptInput()
const previousPromptInput = ref<string | null>(null)
const getPreviousResponseId = (index: number) =>
index > 0 ? (parsedHistory.value[index - 1]?.response_id ?? '') : ''
index > 0 ? parsedHistory.value[index - 1]?.response_id ?? '' : ''
const storePromptInput = () => {
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')

View File

@@ -106,7 +106,7 @@ const getLabel = (val: string | null | undefined) => {
if (val == null) return label ?? ''
if (!options) return label ?? ''
const found = options.find((o) => o.value === val)
return found ? found.name : (label ?? '')
return found ? found.name : label ?? ''
}
// Extract complex style logic from template

View File

@@ -76,7 +76,7 @@ const emit = defineEmits<{
(e: 'click', event: MouseEvent): void
}>()
const overlayValue = computed(() =>
typeof iconBadge === 'function' ? (iconBadge() ?? '') : iconBadge
typeof iconBadge === 'function' ? iconBadge() ?? '' : iconBadge
)
const shouldShowBadge = computed(() => !!overlayValue.value)
const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)

View File

@@ -0,0 +1,33 @@
<template>
<div
class="flex h-full flex-col bg-interface-panel-surface"
:class="props.class"
>
<div>
<div
v-if="slots.top"
class="flex min-h-12 items-center border-b border-interface-stroke px-4 py-2"
>
<slot name="top" />
</div>
<div v-if="slots.header" class="px-4">
<slot name="header" />
</div>
</div>
<!-- h-0 to force scrollpanel to grow -->
<ScrollPanel class="h-0 grow">
<slot name="body" />
</ScrollPanel>
</div>
</template>
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import { useSlots } from 'vue'
const props = defineProps<{
class?: string
}>()
const slots = useSlots()
</script>

View File

@@ -0,0 +1,271 @@
<template>
<AssetsSidebarTemplate>
<template #top>
<span v-if="!isInFolderView" class="font-bold">
{{ $t('sideToolbar.mediaAssets') }}
</span>
<div v-else class="flex w-full items-center justify-between gap-2">
<div class="flex items-center gap-2">
<span class="font-bold">{{ $t('Job ID') }}:</span>
<span class="text-sm">{{ folderPromptId?.substring(0, 8) }}</span>
<button
class="m-0 cursor-pointer border-0 bg-transparent p-0 outline-0"
role="button"
@click="copyJobId"
>
<i class="mb-1 icon-[lucide--copy] text-sm"></i>
</button>
</div>
<div>
<span>{{ formattedExecutionTime }}</span>
</div>
</div>
</template>
<template #header>
<!-- Job Detail View Header -->
<div v-if="isInFolderView" class="pt-4 pb-1">
<IconTextButton
:label="$t('sideToolbar.backToAssets')"
type="secondary"
@click="exitFolderView"
>
<template #icon>
<i class="icon-[lucide--arrow-left] size-4" />
</template>
</IconTextButton>
</div>
<!-- Normal Tab View -->
<TabList v-else v-model="activeTab" class="pt-4 pb-1">
<Tab value="input">{{ $t('sideToolbar.labels.imported') }}</Tab>
<Tab value="output">{{ $t('sideToolbar.labels.generated') }}</Tab>
</TabList>
</template>
<template #body>
<VirtualGrid
v-if="displayAssets.length"
:items="mediaAssetsWithKey"
:grid-style="{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
padding: '0.5rem',
gap: '0.5rem'
}"
>
<template #item="{ item }">
<MediaAssetCard
:asset="item"
:selected="selectedAsset?.id === item.id"
:show-output-count="
activeTab === 'output' &&
!isInFolderView &&
(item.user_metadata?.outputCount as number) > 1
"
:output-count="(item.user_metadata?.outputCount as number) || 0"
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
/>
</template>
</VirtualGrid>
<div v-else-if="loading">
<ProgressSpinner
style="width: 50px; left: 50%; transform: translateX(-50%)"
/>
</div>
<div v-else>
<NoResultsPlaceholder
icon="pi pi-info-circle"
:title="
$t(
activeTab === 'input'
? 'sideToolbar.noImportedFiles'
: 'sideToolbar.noGeneratedFiles'
)
"
:message="$t('sideToolbar.noFilesFoundMessage')"
/>
</div>
</template>
</AssetsSidebarTemplate>
<ResultGallery
v-model:active-index="galleryActiveIndex"
:all-gallery-items="galleryItems"
/>
</template>
<script setup lang="ts">
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, ref, watch } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
import { useMediaAssets } from '@/platform/assets/composables/useMediaAssets'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { ResultItemImpl } from '@/stores/queueStore'
import {
formatDuration,
getMediaTypeFromFilenamePlural
} from '@/utils/formatUtil'
import AssetsSidebarTemplate from './AssetSidebarTemplate.vue'
const activeTab = ref<'input' | 'output'>('input')
const mediaAssets = ref<AssetItem[]>([])
const selectedAsset = ref<AssetItem | null>(null)
const folderPromptId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined)
const isInFolderView = computed(() => folderPromptId.value !== null)
const formattedExecutionTime = computed(() => {
if (!folderExecutionTime.value) return ''
return formatDuration(folderExecutionTime.value * 1000)
})
const toast = useToast()
// Use unified media assets implementation that handles cloud/internal automatically
const { loading, error, fetchMediaList } = useMediaAssets()
const galleryActiveIndex = ref(-1)
const galleryItems = computed(() => {
// Convert AssetItems to ResultItemImpl format for gallery
// Use displayAssets instead of mediaAssets to show correct items based on view mode
return displayAssets.value.map((asset) => {
const resultItem = new ResultItemImpl({
filename: asset.name,
subfolder: '',
type: 'output',
nodeId: '0',
mediaType: getMediaTypeFromFilenamePlural(asset.name)
})
// Override the url getter to use asset.preview_url
Object.defineProperty(resultItem, 'url', {
get() {
return asset.preview_url || ''
},
configurable: true
})
return resultItem
})
})
// Store folder view assets separately
const folderAssets = ref<AssetItem[]>([])
// Get display assets based on view mode
const displayAssets = computed(() => {
if (isInFolderView.value) {
// Show all assets from the folder view
return folderAssets.value
}
// Normal view: show grouped assets (already have outputCount from API)
return mediaAssets.value
})
// Add key property for VirtualGrid
const mediaAssetsWithKey = computed(() => {
return displayAssets.value.map((asset) => ({
...asset,
key: asset.id
}))
})
const refreshAssets = async () => {
const files = await fetchMediaList(activeTab.value)
mediaAssets.value = files
selectedAsset.value = null // Clear selection after refresh
if (error.value) {
console.error('Failed to refresh assets:', error.value)
}
}
watch(activeTab, () => {
void refreshAssets()
})
onMounted(() => {
void refreshAssets()
})
const handleAssetSelect = (asset: AssetItem) => {
// Toggle selection
if (selectedAsset.value?.id === asset.id) {
selectedAsset.value = null
} else {
selectedAsset.value = asset
}
}
const handleZoomClick = (asset: AssetItem) => {
// Find the index of the clicked asset
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
if (index !== -1) {
galleryActiveIndex.value = index
}
}
const enterFolderView = (asset: AssetItem) => {
const promptId = asset.user_metadata?.promptId as string
const allOutputs = asset.user_metadata?.allOutputs as any[]
if (promptId && allOutputs) {
folderPromptId.value = promptId
folderExecutionTime.value = asset.user_metadata
?.executionTimeInSeconds as number
// Convert all outputs to AssetItem format for folder view
folderAssets.value = allOutputs.map((output) => ({
id: `${promptId}-${output.nodeId}-${output.filename}`,
name: output.filename,
size: 0,
created_at: asset.created_at, // Use parent asset's created_at
tags: ['output'],
preview_url: output.url,
user_metadata: {
promptId,
nodeId: output.nodeId,
subfolder: output.subfolder,
executionTimeInSeconds: asset.user_metadata?.executionTimeInSeconds,
workflow: asset.user_metadata?.workflow
}
}))
}
}
const exitFolderView = () => {
folderPromptId.value = null
folderExecutionTime.value = undefined
folderAssets.value = []
}
const copyJobId = async () => {
if (folderPromptId.value) {
try {
await navigator.clipboard.writeText(folderPromptId.value)
toast.add({
severity: 'success',
summary: 'Copied',
detail: 'Job ID copied to clipboard',
life: 2000
})
} catch (error) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to copy Job ID',
life: 3000
})
}
}
}
</script>

View File

@@ -100,9 +100,9 @@ const coverResult = flatOutputs.length
// Using `==` instead of `===` because NodeId can be a string or a number
const node: ComfyNode | null =
flatOutputs.length && props.task.workflow
? (props.task.workflow.nodes.find(
? props.task.workflow.nodes.find(
(n: ComfyNode) => n.id == coverResult?.nodeId
) ?? null)
) ?? null
: null
const progressPreviewBlobUrl = ref('')

View File

@@ -0,0 +1,43 @@
<template>
<button
:class="tabClasses"
role="tab"
:aria-selected="isActive"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup lang="ts">
import type { Ref } from 'vue'
import { computed, inject } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const { value } = defineProps<{
value: string
}>()
const currentValue = inject<Ref<string>>('tabs-value')
const updateValue = inject<(value: string) => void>('tabs-update')
const isActive = computed(() => currentValue?.value === value)
const tabClasses = computed(() => {
return cn(
// Base styles from TextButton
'flex items-center justify-center shrink-0',
'px-2.5 py-2 text-sm rounded-lg cursor-pointer transition-all duration-200',
'outline-hidden border-none',
// State styles with semantic tokens
isActive.value
? 'bg-interface-menu-component-surface-hovered text-text-primary text-bold'
: 'bg-transparent text-text-secondary hover:bg-button-hover-surface focus:bg-button-hover-surface'
)
})
const handleClick = () => {
updateValue?.(value)
}
</script>

View File

@@ -0,0 +1,153 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Tab from './Tab.vue'
import TabList from './TabList.vue'
const meta: Meta<typeof TabList> = {
title: 'Components/Tab/TabList',
component: TabList,
tags: ['autodocs'],
argTypes: {
modelValue: {
control: 'text',
description: 'The currently selected tab value'
},
'onUpdate:modelValue': { action: 'update:modelValue' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: (args) => ({
components: { TabList, Tab },
setup() {
const activeTab = ref(args.modelValue || 'tab1')
return { activeTab }
},
template: `
<TabList v-model="activeTab">
<Tab value="tab1">Tab 1</Tab>
<Tab value="tab2">Tab 2</Tab>
<Tab value="tab3">Tab 3</Tab>
</TabList>
<div class="mt-4 p-4 border rounded">
Selected tab: {{ activeTab }}
</div>
`
}),
args: {
modelValue: 'tab1'
}
}
export const ManyTabs: Story = {
render: () => ({
components: { TabList, Tab },
setup() {
const activeTab = ref('tab1')
return { activeTab }
},
template: `
<TabList v-model="activeTab">
<Tab value="tab1">Dashboard</Tab>
<Tab value="tab2">Analytics</Tab>
<Tab value="tab3">Reports</Tab>
<Tab value="tab4">Settings</Tab>
<Tab value="tab5">Profile</Tab>
</TabList>
<div class="mt-4 p-4 border rounded">
Selected tab: {{ activeTab }}
</div>
`
})
}
export const WithIcons: Story = {
render: () => ({
components: { TabList, Tab },
setup() {
const activeTab = ref('home')
return { activeTab }
},
template: `
<TabList v-model="activeTab">
<Tab value="home">
<i class="pi pi-home mr-2"></i>
Home
</Tab>
<Tab value="users">
<i class="pi pi-users mr-2"></i>
Users
</Tab>
<Tab value="settings">
<i class="pi pi-cog mr-2"></i>
Settings
</Tab>
</TabList>
<div class="mt-4 p-4 border rounded">
Selected tab: {{ activeTab }}
</div>
`
})
}
export const LongLabels: Story = {
render: () => ({
components: { TabList, Tab },
setup() {
const activeTab = ref('overview')
return { activeTab }
},
template: `
<TabList v-model="activeTab">
<Tab value="overview">Project Overview</Tab>
<Tab value="documentation">Documentation & Guides</Tab>
<Tab value="deployment">Deployment Settings</Tab>
<Tab value="monitoring">Monitoring & Analytics</Tab>
</TabList>
<div class="mt-4 p-4 border rounded">
Selected tab: {{ activeTab }}
</div>
`
})
}
export const Interactive: Story = {
render: () => ({
components: { TabList, Tab },
setup() {
const activeTab = ref('input')
const handleTabChange = (value: string) => {
console.log('Tab changed to:', value)
}
return { activeTab, handleTabChange }
},
template: `
<div class="space-y-4">
<div>
<h3 class="text-sm font-semibold mb-2">Example: Media Assets</h3>
<TabList v-model="activeTab" @update:model-value="handleTabChange">
<Tab value="input">Imported</Tab>
<Tab value="output">Generated</Tab>
</TabList>
</div>
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded">
<div v-if="activeTab === 'input'">
<p>Showing imported assets...</p>
</div>
<div v-else-if="activeTab === 'output'">
<p>Showing generated assets...</p>
</div>
</div>
<div class="text-sm text-gray-600">
Current tab value: <code>{{ activeTab }}</code>
</div>
</div>
`
})
}

View File

@@ -0,0 +1,23 @@
<template>
<div class="w-full">
<div class="flex items-center gap-2 pb-1">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { provide, toRef } from 'vue'
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
// Provide for child Tab components
provide('tabs-value', toRef(props, 'modelValue'))
provide('tabs-update', (value: string) => emit('update:modelValue', value))
</script>

View File

@@ -1,38 +0,0 @@
<template>
<TopbarBadge
:badge="cloudBadge"
:display-mode="displayMode"
:reverse-order="reverseOrder"
:no-padding="noPadding"
:background-color="backgroundColor"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { t } from '@/i18n'
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
import TopbarBadge from './TopbarBadge.vue'
withDefaults(
defineProps<{
displayMode?: 'full' | 'compact' | 'icon-only'
reverseOrder?: boolean
noPadding?: boolean
backgroundColor?: string
}>(),
{
displayMode: 'full',
reverseOrder: false,
noPadding: false,
backgroundColor: 'var(--comfy-menu-secondary-bg)'
}
)
const cloudBadge = computed<TopbarBadgeType>(() => ({
label: t('g.beta'),
text: 'Comfy Cloud'
}))
</script>

View File

@@ -69,22 +69,6 @@ vi.mock('@/services/dialogService', () => ({
}))
}))
// Mock the firebaseAuthStore
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: vi.fn(() => ({
getAuthHeader: vi
.fn()
.mockResolvedValue({ Authorization: 'Bearer mock-token' })
}))
}))
// Mock the useSubscription composable
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
useSubscription: vi.fn(() => ({
isActiveSubscription: vi.fn().mockReturnValue(true)
}))
}))
// Mock UserAvatar component
vi.mock('@/components/common/UserAvatar.vue', () => ({
default: {

View File

@@ -67,11 +67,7 @@
</div>
<div class="flex items-center justify-between">
<UserCredit text-class="text-2xl" />
<Button
v-if="isActiveSubscription"
:label="$t('credits.topUp.topUp')"
@click="handleTopUp"
/>
<Button :label="$t('credits.topUp.topUp')" @click="handleTopUp" />
</div>
</div>
</div>
@@ -86,7 +82,6 @@ import UserAvatar from '@/components/common/UserAvatar.vue'
import UserCredit from '@/components/common/UserCredit.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useDialogService } from '@/services/dialogService'
const emit = defineEmits<{
@@ -97,7 +92,6 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
useCurrentUser()
const authActions = useFirebaseAuthActions()
const dialogService = useDialogService()
const { isActiveSubscription } = useSubscription()
const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user')

Some files were not shown because too many files have changed in this diff Show More