Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0728175
Align headers
benceruleanlu Jul 8, 2025
1c3d117
Correct positioning of collapse button
benceruleanlu Jul 8, 2025
e0ef8ca
Correct rounding
benceruleanlu Jul 8, 2025
cdc2654
Update locales [skip ci]
invalid-email-address Jul 8, 2025
5a8e7f7
Adds basic collapsing
benceruleanlu Jul 8, 2025
14b3300
Sync collapsing with litegraph
benceruleanlu Jul 8, 2025
233ecd7
Change hidden to disabled
benceruleanlu Jul 8, 2025
e411fea
Revert "Change hidden to disabled"
benceruleanlu Jul 8, 2025
fbb0421
v-show not v-if
benceruleanlu Jul 8, 2025
36e8d2d
Add title editing functionality
benceruleanlu Jul 9, 2025
8a6f877
Remove watch for now
benceruleanlu Jul 9, 2025
54e6771
Remove dead 'title-edit' emit from NodeHeader
benceruleanlu Jul 9, 2025
dc78933
Replace hardcoded -30px margin with NODE_TITLE_HEIGHT constant
benceruleanlu Jul 9, 2025
74867db
Add title watcher
benceruleanlu Jul 10, 2025
6d018d1
Add collapsed state watcher
benceruleanlu Jul 10, 2025
2dba60c
Use node.collapse()
benceruleanlu Jul 10, 2025
fe215db
Remove logic handling from display component
benceruleanlu Jul 10, 2025
ec4a355
Remove title attribute from collapse/expand button in NodeHeader.vue
benceruleanlu Jul 10, 2025
57f46ad
Temporarily remove CLAUDE.md
benceruleanlu Jul 18, 2025
e7f8d44
Add support for property change events in graph node manager
benceruleanlu Jul 20, 2025
b791d95
Fix double click propagation bug
benceruleanlu Jul 20, 2025
9ba09ec
Refactor jank offset
benceruleanlu Jul 20, 2025
cb13491
Vue Node Widgets (#4360)
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
  •  
  •  
  •  
Prev Previous commit
Vue Node Widgets (#4360)
Co-authored-by: Claude <[email protected]>
  • Loading branch information
benceruleanlu and claude authored Aug 6, 2025
commit cb13491ada77c2b1bba854ed970c9dd8b0cc405c
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