Skip to content

Vue Node Widgets #4360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Aug 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c03526d
Add basic styling to textarea
benceruleanlu Jul 5, 2025
2b9a401
Add hover outlines to select button and toggleswitch
benceruleanlu Jul 5, 2025
80ed69f
Formatting of Select and Slider
benceruleanlu Jul 5, 2025
519ea12
Style ToggleSwitch
benceruleanlu Jul 5, 2025
0d2678d
Add number to slider
benceruleanlu Jul 5, 2025
ab8f1a8
Realign gap values
benceruleanlu Jul 5, 2025
d61d15b
Style InputText
benceruleanlu Jul 5, 2025
f288c99
Fix options sizing
benceruleanlu Jul 5, 2025
7628f1e
Style MultiSelect
benceruleanlu Jul 5, 2025
f5d9e8d
Style Select Button
benceruleanlu Jul 5, 2025
371026d
Adjust sizings and leave explanatory comments
benceruleanlu Jul 5, 2025
e0c9c87
Remove number spinner
benceruleanlu Jul 5, 2025
097e124
Work with precision in slider widget
benceruleanlu Jul 5, 2025
b84bb6a
Fix value bindings
benceruleanlu Jul 5, 2025
f8d6cf9
Fix infinite callback loop and equality check
benceruleanlu Jul 7, 2025
f25ae3e
Add markdown widget
benceruleanlu Jul 7, 2025
e834883
Change fileupload to custom impl
benceruleanlu Jul 14, 2025
a2dfe1e
Fileupload widget (WIP)
benceruleanlu Jul 16, 2025
b6d60b6
Remove slider animations
benceruleanlu Jul 16, 2025
92d59b9
Add imagecompare and galleria to enum
benceruleanlu Jul 16, 2025
0da8757
Remove labels from imagecompare
benceruleanlu Jul 16, 2025
cdca65b
Remove divider border
benceruleanlu Jul 16, 2025
36c33c9
Remove gallery indicators
benceruleanlu Jul 16, 2025
6b42d7a
Align formatting of gallery
benceruleanlu Jul 16, 2025
e0fc38c
Fix spacing formatting issue for gallery
benceruleanlu Jul 16, 2025
73ba63d
Remove label from gallery
benceruleanlu Jul 16, 2025
2bf8ee6
Fix image compare
benceruleanlu Jul 16, 2025
80c4f76
Install chart.js
benceruleanlu Jul 16, 2025
f65856a
Add chart widget
benceruleanlu Jul 17, 2025
071ebf7
nit
benceruleanlu Jul 20, 2025
90ce0d1
Add lowercase fileupload to widget type enum
benceruleanlu Jul 21, 2025
ff0b9a2
Add vue widgets to ComfyWidgets
benceruleanlu Jul 22, 2025
e912f45
Add tentative widget specs
benceruleanlu Jul 23, 2025
8a607d5
Fix chart widget spec
benceruleanlu Jul 23, 2025
9d7f6bb
Add chart widget composable
benceruleanlu Jul 23, 2025
10c7a15
Add color widget composable
benceruleanlu Jul 23, 2025
097d0d9
coupling? I've never heard of it
benceruleanlu Jul 23, 2025
d9dc6b0
Tentatively add rest of widget composables
benceruleanlu Jul 23, 2025
36002be
Vue Nodes Slots (#4515)
benceruleanlu Aug 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
131 changes: 131 additions & 0 deletions browser_tests/fixtures/utils/vueNodeFixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { Locator, Page } from '@playwright/test'

import type { NodeReference } from './litegraphUtils'

/**
* VueNodeFixture provides Vue-specific testing utilities for interacting with
* Vue node components. It bridges the gap between litegraph node references
* and Vue UI components.
*/
export class VueNodeFixture {
constructor(
private readonly nodeRef: NodeReference,
private readonly page: Page
) {}

/**
* Get the node's header element using data-testid
*/
async getHeader(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
}

/**
* Get the node's title element
*/
async getTitleElement(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-title"]')
}

/**
* Get the current title text
*/
async getTitle(): Promise<string> {
const titleElement = await this.getTitleElement()
return (await titleElement.textContent()) || ''
}

/**
* Set a new title by double-clicking and entering text
*/
async setTitle(newTitle: string): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()

const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill(newTitle)
await input.press('Enter')
}

/**
* Cancel title editing
*/
async cancelTitleEdit(): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()

const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.press('Escape')
}

/**
* Check if the title is currently being edited
*/
async isEditingTitle(): Promise<boolean> {
const header = await this.getHeader()
const input = header.locator('[data-testid="node-title-input"]')
return await input.isVisible()
}

/**
* Get the collapse/expand button
*/
async getCollapseButton(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-collapse-button"]')
}

/**
* Toggle the node's collapsed state
*/
async toggleCollapse(): Promise<void> {
const button = await this.getCollapseButton()
await button.click()
}

/**
* Get the collapse icon element
*/
async getCollapseIcon(): Promise<Locator> {
const button = await this.getCollapseButton()
return button.locator('i')
}

/**
* Get the collapse icon's CSS classes
*/
async getCollapseIconClass(): Promise<string> {
const icon = await this.getCollapseIcon()
return (await icon.getAttribute('class')) || ''
}

/**
* Check if the collapse button is visible
*/
async isCollapseButtonVisible(): Promise<boolean> {
const button = await this.getCollapseButton()
return await button.isVisible()
}

/**
* Get the node's body/content element
*/
async getBody(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
}

/**
* Check if the node body is visible (not collapsed)
*/
async isBodyVisible(): Promise<boolean> {
const body = await this.getBody()
return await body.isVisible()
}
}
138 changes: 138 additions & 0 deletions browser_tests/tests/vueNodes/NodeHeader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'

test.describe('NodeHeader', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setup()
// Load single SaveImage node workflow (positioned below menu bar)
await comfyPage.loadWorkflow('single_save_image_node')
})

test('displays node title', async ({ comfyPage }) => {
// Get the single SaveImage node from the workflow
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
expect(nodes.length).toBeGreaterThanOrEqual(1)

const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

const title = await vueNode.getTitle()
expect(title).toBe('Save Image')

// Verify title is visible in the header
const header = await vueNode.getHeader()
await expect(header).toContainText('Save Image')
})

test('allows title renaming', async ({ comfyPage }) => {
// Get the single SaveImage node from the workflow
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Test renaming with Enter
await vueNode.setTitle('My Custom Sampler')
const newTitle = await vueNode.getTitle()
expect(newTitle).toBe('My Custom Sampler')

// Verify the title is displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('My Custom Sampler')

// Test cancel with Escape
const titleElement = await vueNode.getTitleElement()
await titleElement.dblclick()
await comfyPage.nextFrame()

// Type a different value but cancel
const input = (await vueNode.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill('This Should Be Cancelled')
await input.press('Escape')
await comfyPage.nextFrame()

// Title should remain as the previously saved value
const titleAfterCancel = await vueNode.getTitle()
expect(titleAfterCancel).toBe('My Custom Sampler')
})

test('handles node collapsing', async ({ comfyPage }) => {
// Get the single SaveImage node from the workflow
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Initially should not be collapsed
expect(await node.isCollapsed()).toBe(false)
const body = await vueNode.getBody()
await expect(body).toBeVisible()

// Collapse the node
await vueNode.toggleCollapse()
expect(await node.isCollapsed()).toBe(true)

// Verify node content is hidden
const collapsedSize = await node.getSize()
await expect(body).not.toBeVisible()

// Expand again
await vueNode.toggleCollapse()
expect(await node.isCollapsed()).toBe(false)
await expect(body).toBeVisible()

// Size should be restored
const expandedSize = await node.getSize()
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
})

test('shows collapse/expand icon state', async ({ comfyPage }) => {
// Get the single SaveImage node from the workflow
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Check initial expanded state icon
let iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down')

// Collapse and check icon
await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-right')

// Expand and check icon
await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down')
})

test('preserves title when collapsing/expanding', async ({ comfyPage }) => {
// Get the single SaveImage node from the workflow
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Set custom title
await vueNode.setTitle('Test Sampler')
expect(await vueNode.getTitle()).toBe('Test Sampler')

// Collapse
await vueNode.toggleCollapse()
expect(await vueNode.getTitle()).toBe('Test Sampler')

// Expand
await vueNode.toggleCollapse()
expect(await vueNode.getTitle()).toBe('Test Sampler')

// Verify title is still displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('Test Sampler')
})
})
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default [
ignores: [
'src/scripts/*',
'src/extensions/core/*',
'src/types/vue-shim.d.ts'
'src/types/vue-shim.d.ts',
'src/lib/litegraph/**/*'
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export default {

'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
...formatAndEslint(stagedFiles),
'vue-tsc --noEmit'
'npm run typecheck'
]
}

Expand Down
26 changes: 19 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "npm run typecheck && vite build",
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit",
"typecheck": "vue-tsc --build",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:browser": "npx playwright test",
Expand Down Expand Up @@ -76,7 +76,6 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@comfyorg/litegraph": "^0.16.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
Expand All @@ -93,6 +92,7 @@
"@xterm/xterm": "^5.5.0",
"algoliasearch": "^5.21.0",
"axios": "^1.8.2",
"chart.js": "^4.5.0",
"dompurify": "^3.2.5",
"dotenv": "^16.4.5",
"firebase": "^11.6.0",
Expand Down
Loading