Skip to content

Commit cb13491

Browse files
Vue Node Widgets (#4360)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9ba09ec commit cb13491

File tree

316 files changed

+37285
-728
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

316 files changed

+37285
-728
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { Locator, Page } from '@playwright/test'
2+
3+
import type { NodeReference } from './litegraphUtils'
4+
5+
/**
6+
* VueNodeFixture provides Vue-specific testing utilities for interacting with
7+
* Vue node components. It bridges the gap between litegraph node references
8+
* and Vue UI components.
9+
*/
10+
export class VueNodeFixture {
11+
constructor(
12+
private readonly nodeRef: NodeReference,
13+
private readonly page: Page
14+
) {}
15+
16+
/**
17+
* Get the node's header element using data-testid
18+
*/
19+
async getHeader(): Promise<Locator> {
20+
const nodeId = this.nodeRef.id
21+
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
22+
}
23+
24+
/**
25+
* Get the node's title element
26+
*/
27+
async getTitleElement(): Promise<Locator> {
28+
const header = await this.getHeader()
29+
return header.locator('[data-testid="node-title"]')
30+
}
31+
32+
/**
33+
* Get the current title text
34+
*/
35+
async getTitle(): Promise<string> {
36+
const titleElement = await this.getTitleElement()
37+
return (await titleElement.textContent()) || ''
38+
}
39+
40+
/**
41+
* Set a new title by double-clicking and entering text
42+
*/
43+
async setTitle(newTitle: string): Promise<void> {
44+
const titleElement = await this.getTitleElement()
45+
await titleElement.dblclick()
46+
47+
const input = (await this.getHeader()).locator(
48+
'[data-testid="node-title-input"]'
49+
)
50+
await input.fill(newTitle)
51+
await input.press('Enter')
52+
}
53+
54+
/**
55+
* Cancel title editing
56+
*/
57+
async cancelTitleEdit(): Promise<void> {
58+
const titleElement = await this.getTitleElement()
59+
await titleElement.dblclick()
60+
61+
const input = (await this.getHeader()).locator(
62+
'[data-testid="node-title-input"]'
63+
)
64+
await input.press('Escape')
65+
}
66+
67+
/**
68+
* Check if the title is currently being edited
69+
*/
70+
async isEditingTitle(): Promise<boolean> {
71+
const header = await this.getHeader()
72+
const input = header.locator('[data-testid="node-title-input"]')
73+
return await input.isVisible()
74+
}
75+
76+
/**
77+
* Get the collapse/expand button
78+
*/
79+
async getCollapseButton(): Promise<Locator> {
80+
const header = await this.getHeader()
81+
return header.locator('[data-testid="node-collapse-button"]')
82+
}
83+
84+
/**
85+
* Toggle the node's collapsed state
86+
*/
87+
async toggleCollapse(): Promise<void> {
88+
const button = await this.getCollapseButton()
89+
await button.click()
90+
}
91+
92+
/**
93+
* Get the collapse icon element
94+
*/
95+
async getCollapseIcon(): Promise<Locator> {
96+
const button = await this.getCollapseButton()
97+
return button.locator('i')
98+
}
99+
100+
/**
101+
* Get the collapse icon's CSS classes
102+
*/
103+
async getCollapseIconClass(): Promise<string> {
104+
const icon = await this.getCollapseIcon()
105+
return (await icon.getAttribute('class')) || ''
106+
}
107+
108+
/**
109+
* Check if the collapse button is visible
110+
*/
111+
async isCollapseButtonVisible(): Promise<boolean> {
112+
const button = await this.getCollapseButton()
113+
return await button.isVisible()
114+
}
115+
116+
/**
117+
* Get the node's body/content element
118+
*/
119+
async getBody(): Promise<Locator> {
120+
const nodeId = this.nodeRef.id
121+
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
122+
}
123+
124+
/**
125+
* Check if the node body is visible (not collapsed)
126+
*/
127+
async isBodyVisible(): Promise<boolean> {
128+
const body = await this.getBody()
129+
return await body.isVisible()
130+
}
131+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
comfyExpect as expect,
3+
comfyPageFixture as test
4+
} from '../../fixtures/ComfyPage'
5+
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
6+
7+
test.describe('NodeHeader', () => {
8+
test.beforeEach(async ({ comfyPage }) => {
9+
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
10+
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
11+
await comfyPage.setSetting('Comfy.EnableTooltips', true)
12+
await comfyPage.setup()
13+
// Load single SaveImage node workflow (positioned below menu bar)
14+
await comfyPage.loadWorkflow('single_save_image_node')
15+
})
16+
17+
test('displays node title', async ({ comfyPage }) => {
18+
// Get the single SaveImage node from the workflow
19+
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
20+
expect(nodes.length).toBeGreaterThanOrEqual(1)
21+
22+
const node = nodes[0]
23+
const vueNode = new VueNodeFixture(node, comfyPage.page)
24+
25+
const title = await vueNode.getTitle()
26+
expect(title).toBe('Save Image')
27+
28+
// Verify title is visible in the header
29+
const header = await vueNode.getHeader()
30+
await expect(header).toContainText('Save Image')
31+
})
32+
33+
test('allows title renaming', async ({ comfyPage }) => {
34+
// Get the single SaveImage node from the workflow
35+
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
36+
const node = nodes[0]
37+
const vueNode = new VueNodeFixture(node, comfyPage.page)
38+
39+
// Test renaming with Enter
40+
await vueNode.setTitle('My Custom Sampler')
41+
const newTitle = await vueNode.getTitle()
42+
expect(newTitle).toBe('My Custom Sampler')
43+
44+
// Verify the title is displayed
45+
const header = await vueNode.getHeader()
46+
await expect(header).toContainText('My Custom Sampler')
47+
48+
// Test cancel with Escape
49+
const titleElement = await vueNode.getTitleElement()
50+
await titleElement.dblclick()
51+
await comfyPage.nextFrame()
52+
53+
// Type a different value but cancel
54+
const input = (await vueNode.getHeader()).locator(
55+
'[data-testid="node-title-input"]'
56+
)
57+
await input.fill('This Should Be Cancelled')
58+
await input.press('Escape')
59+
await comfyPage.nextFrame()
60+
61+
// Title should remain as the previously saved value
62+
const titleAfterCancel = await vueNode.getTitle()
63+
expect(titleAfterCancel).toBe('My Custom Sampler')
64+
})
65+
66+
test('handles node collapsing', async ({ comfyPage }) => {
67+
// Get the single SaveImage node from the workflow
68+
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
69+
const node = nodes[0]
70+
const vueNode = new VueNodeFixture(node, comfyPage.page)
71+
72+
// Initially should not be collapsed
73+
expect(await node.isCollapsed()).toBe(false)
74+
const body = await vueNode.getBody()
75+
await expect(body).toBeVisible()
76+
77+
// Collapse the node
78+
await vueNode.toggleCollapse()
79+
expect(await node.isCollapsed()).toBe(true)
80+
81+
// Verify node content is hidden
82+
const collapsedSize = await node.getSize()
83+
await expect(body).not.toBeVisible()
84+
85+
// Expand again
86+
await vueNode.toggleCollapse()
87+
expect(await node.isCollapsed()).toBe(false)
88+
await expect(body).toBeVisible()
89+
90+
// Size should be restored
91+
const expandedSize = await node.getSize()
92+
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
93+
})
94+
95+
test('shows collapse/expand icon state', async ({ comfyPage }) => {
96+
// Get the single SaveImage node from the workflow
97+
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
98+
const node = nodes[0]
99+
const vueNode = new VueNodeFixture(node, comfyPage.page)
100+
101+
// Check initial expanded state icon
102+
let iconClass = await vueNode.getCollapseIconClass()
103+
expect(iconClass).toContain('pi-chevron-down')
104+
105+
// Collapse and check icon
106+
await vueNode.toggleCollapse()
107+
iconClass = await vueNode.getCollapseIconClass()
108+
expect(iconClass).toContain('pi-chevron-right')
109+
110+
// Expand and check icon
111+
await vueNode.toggleCollapse()
112+
iconClass = await vueNode.getCollapseIconClass()
113+
expect(iconClass).toContain('pi-chevron-down')
114+
})
115+
116+
test('preserves title when collapsing/expanding', async ({ comfyPage }) => {
117+
// Get the single SaveImage node from the workflow
118+
const nodes = await comfyPage.getNodeRefsByType('SaveImage')
119+
const node = nodes[0]
120+
const vueNode = new VueNodeFixture(node, comfyPage.page)
121+
122+
// Set custom title
123+
await vueNode.setTitle('Test Sampler')
124+
expect(await vueNode.getTitle()).toBe('Test Sampler')
125+
126+
// Collapse
127+
await vueNode.toggleCollapse()
128+
expect(await vueNode.getTitle()).toBe('Test Sampler')
129+
130+
// Expand
131+
await vueNode.toggleCollapse()
132+
expect(await vueNode.getTitle()).toBe('Test Sampler')
133+
134+
// Verify title is still displayed
135+
const header = await vueNode.getHeader()
136+
await expect(header).toContainText('Test Sampler')
137+
})
138+
})

eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export default [
1313
ignores: [
1414
'src/scripts/*',
1515
'src/extensions/core/*',
16-
'src/types/vue-shim.d.ts'
16+
'src/types/vue-shim.d.ts',
17+
'src/lib/litegraph/**/*'
1718
]
1819
},
1920
{

lint-staged.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export default {
33

44
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
55
...formatAndEslint(stagedFiles),
6-
'vue-tsc --noEmit'
6+
'npm run typecheck'
77
]
88
}
99

package-lock.json

Lines changed: 19 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"build": "npm run typecheck && vite build",
1414
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
1515
"zipdist": "node scripts/zipdist.js",
16-
"typecheck": "vue-tsc --noEmit",
16+
"typecheck": "vue-tsc --build",
1717
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
1818
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
1919
"test:browser": "npx playwright test",
@@ -76,7 +76,6 @@
7676
"@alloc/quick-lru": "^5.2.0",
7777
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
7878
"@comfyorg/comfyui-electron-types": "^0.4.43",
79-
"@comfyorg/litegraph": "^0.16.3",
8079
"@primevue/forms": "^4.2.5",
8180
"@primevue/themes": "^4.2.5",
8281
"@sentry/vue": "^8.48.0",
@@ -93,6 +92,7 @@
9392
"@xterm/xterm": "^5.5.0",
9493
"algoliasearch": "^5.21.0",
9594
"axios": "^1.8.2",
95+
"chart.js": "^4.5.0",
9696
"dompurify": "^3.2.5",
9797
"dotenv": "^16.4.5",
9898
"firebase": "^11.6.0",

0 commit comments

Comments
 (0)