Compare commits

...

53 Commits

Author SHA1 Message Date
Terry Jia
84c5f7bf16 [3d] add output preview screen for load3d node 2025-02-07 19:14:34 -05:00
bymyself
a914456827 Add support for new COMBO input spec and lazy/remote COMBO widgets (#2422) 2025-02-07 15:35:42 -05:00
Chenlei Hu
340513e27f Type INodeSlot.widget (#2463) 2025-02-07 14:33:21 -05:00
bymyself
117c8be3a0 Use fp16 safetensors model in default/fallback workflow (#2433)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-06 19:47:14 -05:00
Chenlei Hu
68f6d51ad2 [i18n] Translate clipboard toast messages (#2462)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-06 15:34:15 -05:00
Chenlei Hu
2da23fd373 [Refactor] Extract litegraph settings sync to a composable (#2461) 2025-02-06 15:21:11 -05:00
Chenlei Hu
7ddcac88d7 [Refactor] Upstream drag zoom feature to litegraph (#2459) 2025-02-06 14:54:36 -05:00
Chenlei Hu
40a817bb0f 1.9.6 (#2460) 2025-02-06 14:48:57 -05:00
filtered
774ed4178f [Cleanup] Fix file and variable names to match usage (#2458) 2025-02-06 14:48:24 -05:00
filtered
78e4161c51 [Refactor] Terminal output drawer shared component (#2457) 2025-02-06 14:33:12 -05:00
filtered
dda9a72966 [Desktop] Add desktop updating page (#2454)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-06 14:23:51 -05:00
Chenlei Hu
d7673af8f5 [Refactor] Move resetView to litegraphService (#2456) 2025-02-06 14:22:40 -05:00
Chenlei Hu
2b18949615 [Cleanup] Remove duplicated group header drawing logic (#2453) 2025-02-06 13:52:05 -05:00
Chenlei Hu
629ac63f06 Type LGraphNode.pasteFile (#2455) 2025-02-06 13:50:48 -05:00
Chenlei Hu
1061620783 [Refactor] Move paste handling to usePaste composable (#2452) 2025-02-06 13:37:37 -05:00
Chenlei Hu
af7a6601e0 [Refactor] Move copy handling to useCopy composable (#2451) 2025-02-06 13:22:48 -05:00
Chenlei Hu
0e0c4b1302 [Refactor] Move isImageNode to litegraphUtil (#2450) 2025-02-06 13:15:17 -05:00
Chenlei Hu
fb170c9ee9 1.9.5 (#2448) 2025-02-06 10:49:37 -05:00
Chenlei Hu
7ef304b381 Fix litegraph copy in Chrome 133 (#2446) 2025-02-06 10:18:28 -05:00
Chenlei Hu
50833341bb 1.9.4 (#2444) 2025-02-05 22:25:19 -05:00
kvick-games
f5c5a95bdc Refresh Preview3D node with node.onMouseEnter (#2439) 2025-02-05 21:26:45 -05:00
Chenlei Hu
b700cc1824 [Documentation] Add src/scripts/README.md (#2442) 2025-02-05 18:32:44 -05:00
Chenlei Hu
a6031ec2be [Cleanup] Rename usePragmaticDroppable to usePragmaticDragAndDrop (#2441) 2025-02-05 17:46:28 -05:00
Chenlei Hu
a4d99d9d28 Support insert workflow by dragging from workflow sidebar to canvas (#2440) 2025-02-05 17:09:01 -05:00
Chenlei Hu
5a7465a907 Add node drag preview for bookmarked nodes (#2438) 2025-02-05 16:25:56 -05:00
Chenlei Hu
6525ae7cf4 [Refactor] Rename hooks/ to composables/ (#2437) 2025-02-05 15:05:56 -05:00
Chenlei Hu
c6ef107111 [Style] Fix shrinking of extension added topmenu items (#2436) 2025-02-05 13:40:14 -05:00
bymyself
3fbccd20ff Use fp16 model in default img2img template workflow (#2430) 2025-02-05 13:27:16 -05:00
Chenlei Hu
fa0682d66e 1.9.3 (#2434) 2025-02-05 13:26:56 -05:00
Chenlei Hu
f7b613c6cb 1.9.2 (#2429)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-05 12:20:20 -05:00
Chenlei Hu
292af3fe3f [Refactor] Replace explicit 'node' param with 'this' for TreeExplorer (#2427) 2025-02-05 12:20:08 -05:00
KarryCharon
2c12df12ab [i18n] Impl i18n context menu translation (#2425)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-02-05 12:07:40 -05:00
Chenlei Hu
d0e99beaa7 Swap eslint/prettier trigger order in pre-commit (#2428) 2025-02-05 12:04:20 -05:00
Chenlei Hu
6b64b74f6c [Desktop] Use fallback mirrors for China users (#2424) 2025-02-05 10:51:32 -05:00
Chenlei Hu
0af4768dd2 Update litegraph 0.8.67 (#2423) 2025-02-04 19:31:09 -05:00
Chenlei Hu
5f850ddaa4 [BrowserTest] Add test on boolean widget (#2421)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-04 17:28:32 -05:00
dependabot[bot]
9fb3235df4 Bump vitest from 2.0.5 to 2.1.9 (#2420)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 17:11:56 -05:00
Chenlei Hu
628facaa75 1.9.1 (#2416) 2025-02-03 20:55:06 -05:00
Chenlei Hu
eb5a4b65ab Update litegraph 0.8.66 (#2415) 2025-02-03 20:30:01 -05:00
Chenlei Hu
98c197e8b1 Add LiteGraph.Canvas.LowQualityRenderingZoomThreshold setting (#2412)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-03 20:28:07 -05:00
Chenlei Hu
90914a40ba Use canvas.low_quality flag for DOMWidget show/hide (#2413) 2025-02-03 20:23:07 -05:00
Chenlei Hu
821816955f Update litegraph 0.8.65 (#2411) 2025-02-03 17:38:29 -05:00
Chenlei Hu
b4121008cd Fix NoteNode constructor fields (#2410) 2025-02-03 14:20:39 -05:00
Chenlei Hu
3730c2b36f [Refactor] Use litegraph.strokeShape (#2406) 2025-02-02 23:15:09 -05:00
bymyself
77be5ac514 [BrowserTest] Add test on groupnode with duplicate hidden inputs (#2403) 2025-02-02 21:03:08 -05:00
bymyself
83759b9a4a Use command label as header text in change keybinding dialog (#2404) 2025-02-02 21:02:48 -05:00
Chenlei Hu
b8f187713e 1.9.0 (#2401) 2025-02-02 17:35:04 -05:00
filtered
b8088ad782 Revert "Correct node/output titles when grouping nodes (#2359) (#2370)" (#2399)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-02 17:23:55 -05:00
bymyself
a37671b154 Add bottom panel to extension manager (#2393) 2025-02-02 15:01:42 -05:00
filtered
57bc7ad312 Add node tooltip delay setting (#2396)
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-02-02 14:31:34 -05:00
filtered
5f59fbdead [Desktop] Persist troubleshooting terminal when hidden (#2398)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-02 14:29:47 -05:00
filtered
4eed9c7e53 [Accessibility] Use keybindings to add keybindings (#2384) 2025-01-31 09:46:23 -08:00
bymyself
ee6197785a Fix typo in coreSettings.ts (#2391)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-31 07:50:16 -08:00
241 changed files with 3297 additions and 951 deletions

View File

@@ -15,7 +15,7 @@ const folderStructure = `
src/
components/
constants/
hooks/
composables/
views/
stores/
services/

View File

@@ -114,7 +114,7 @@
{ "name": "VAE", "type": "VAE", "links": [8], "slot_index": 2 }
],
"properties": {},
"widgets_values": ["v1-5-pruned-emaonly.ckpt"]
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
}
],
"links": [

View File

@@ -0,0 +1,163 @@
{
"last_node_id": 19,
"last_link_id": 14,
"nodes": [
{
"id": 19,
"type": "workflow>two_VAE_decode",
"pos": [
1368.800048828125,
768.7999877929688
],
"size": [
418.1999816894531,
86
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "VAEDecode IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {}
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
},
"node_versions": {},
"ue_links": [],
"groupNodes": {
"two_VAE_decode": {
"nodes": [
{
"id": -1,
"type": "VAEDecode",
"pos": [
1368.800048828125,
768.7999877929688
],
"size": [
210,
46
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null,
"localized_name": "samples"
},
{
"name": "vae",
"type": "VAE",
"link": null,
"localized_name": "vae"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"localized_name": "IMAGE"
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"index": 0
},
{
"id": -1,
"type": "VAEDecode",
"pos": [
1368.800048828125,
873.7999877929688
],
"size": [
210,
46
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null,
"localized_name": "samples"
},
{
"name": "vae",
"type": "VAE",
"link": null,
"localized_name": "vae"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"localized_name": "IMAGE"
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"index": 1
}
],
"links": [],
"external": [],
"config": {
"1": {
"input": {
"samples": {
"visible": false
},
"vae": {
"visible": false
}
}
}
}
}
}
},
"version": 0.4
}

View File

@@ -0,0 +1,48 @@
{
"last_node_id": 15,
"last_link_id": 10,
"nodes": [
{
"id": 15,
"type": "DevToolsRemoteWidgetNode",
"pos": [
495,
735
],
"size": [
315,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsRemoteWidgetNode"
},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8008869919566275,
"offset": [
538.9801226576359,
-55.24554581806672
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,35 @@
{
"last_node_id": 11,
"last_link_id": 9,
"nodes": [
{
"id": 11,
"type": "DevToolsNodeWithBooleanInput",
"pos": [
0,
30
],
"size": [
315,
58
],
"flags": {
"collapsed": false
},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithBooleanInput"
},
"widgets_values": [
false
]
}
],
"links": [],
"groups": [],
"config": {},
"version": 0.4
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -253,7 +253,7 @@ test.describe('Settings', () => {
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('Comfy.NewBlankWorkflow')
.getByLabel('New Blank Workflow')
.getByLabel('Save')
await saveButton.click()

View File

@@ -83,6 +83,12 @@ export class NodeWidgetReference {
y: pos[1]
}
}
async click() {
await this.node.comfyPage.canvas.click({
position: await this.getPosition()
})
}
}
export class NodeReference {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -134,6 +134,37 @@ test.describe('Group Node', () => {
expect(await manage2.getSelectedNodeType()).toBe('g2')
})
test('Preserves hidden input configuration when containing duplicate node types', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('group_node_identical_nodes_hidden_inputs')
await comfyPage.nextFrame()
const groupNodeId = 19
const groupNodeName = 'two_VAE_decode'
const totalInputCount = await comfyPage.page.evaluate((nodeName) => {
const {
extra: { groupNodes }
} = window['app'].graph
const { nodes } = groupNodes[nodeName]
return nodes.reduce((acc: number, node) => {
return acc + node.inputs.length
}, 0)
}, groupNodeName)
const visibleInputCount = await comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id)
return node.inputs.length
}, groupNodeId)
// Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each)
expect(totalInputCount).toBe(4)
// Verify there are 2 visible inputs (2 have been hidden in config)
expect(visibleInputCount).toBe(2)
})
test('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 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: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -707,6 +707,36 @@ test.describe('Menu', () => {
'*Unsaved Workflow.json'
])
})
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
await comfyPage.setup()
await comfyPage.menu.workflowsTab.open()
const nodeCount = await comfyPage.getGraphNodesCount()
// Get the bounding box of the canvas element
const canvasBoundingBox = (await comfyPage.page
.locator('#graph-canvas')
.boundingBox())!
// Calculate the center position of the canvas
const targetPosition = {
x: canvasBoundingBox.x + canvasBoundingBox.width / 2,
y: canvasBoundingBox.y + canvasBoundingBox.height / 2
}
await comfyPage.page.dragAndDrop(
'.comfyui-workflows-browse .node-label:has-text("workflow1.json")',
'#graph-canvas',
{ targetPosition }
)
// Wait for the workflow to be inserted
await comfyPage.page.waitForTimeout(200)
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount * 2)
})
})
test.describe('Workflows topbar tabs', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1,258 @@
import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Remote COMBO Widget', () => {
const mockOptions = ['d', 'c', 'b', 'a']
const addRemoteWidgetNode = async (
comfyPage: ComfyPage,
nodeName: string,
count: number = 1
) => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.open()
await tab.getFolder('DevTools').click()
const nodeEntry = tab.getNode(nodeName).first()
for (let i = 0; i < count; i++) {
await nodeEntry.click()
await comfyPage.nextFrame()
}
}
const getWidgetOptions = async (
comfyPage: ComfyPage,
nodeName: string
): Promise<string[] | undefined> => {
return await comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name)
return node.widgets[0].options.values
}, nodeName)
}
const waitForWidgetUpdate = async (comfyPage: ComfyPage) => {
// Force re-render to trigger first access of widget's options
await comfyPage.page.mouse.click(100, 100)
await comfyPage.page.waitForTimeout(256)
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.describe('Loading options', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route, request) => {
const params = new URL(request.url()).searchParams
const sort = params.get('sort')
await route.fulfill({
body: JSON.stringify(sort ? [...mockOptions].sort() : mockOptions),
status: 200
})
}
)
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.page.unroute('**/api/models/checkpoints**')
})
test('lazy loads options when widget is added from node library', async ({
comfyPage
}) => {
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
const widgetOptions = await getWidgetOptions(comfyPage, nodeName)
expect(widgetOptions).toEqual(mockOptions)
})
test('lazy loads options when widget is added via workflow load', async ({
comfyPage
}) => {
const nodeName = 'Remote Widget Node'
await comfyPage.loadWorkflow('remote_widget')
await comfyPage.page.waitForTimeout(512)
const node = await comfyPage.page.evaluate((name) => {
return window['app'].graph.nodes.find((node) => node.title === name)
}, nodeName)
expect(node).toBeDefined()
await waitForWidgetUpdate(comfyPage)
const widgetOptions = await getWidgetOptions(comfyPage, nodeName)
expect(widgetOptions).toEqual(mockOptions)
})
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)
const widgetOptions = await getWidgetOptions(comfyPage, nodeName)
expect(widgetOptions).not.toEqual(mockOptions)
expect(widgetOptions).toEqual([...mockOptions].sort())
})
test('handles empty list of options', async ({ comfyPage }) => {
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
await route.fulfill({ body: JSON.stringify([]), status: 200 })
}
)
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
const widgetOptions = await getWidgetOptions(comfyPage, nodeName)
expect(widgetOptions).toEqual([])
})
test('falls back to default value when non-200 response', async ({
comfyPage
}) => {
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
await route.fulfill({ status: 500 })
}
)
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
const widgetOptions = await getWidgetOptions(comfyPage, nodeName)
const defaultValue = 'Loading...'
expect(widgetOptions).toEqual(defaultValue)
})
})
test.describe('Lazy Loading Behavior', () => {
test('does not fetch options before widget is added to graph', async ({
comfyPage
}) => {
let requestWasMade = false
comfyPage.page.on('request', (request) => {
if (request.url().includes('/api/models/checkpoints')) {
requestWasMade = true
}
})
// Wait a reasonable time to ensure no request is made
await comfyPage.page.waitForTimeout(512)
expect(requestWasMade).toBe(false)
})
test('fetches options immediately after widget is added to graph', async ({
comfyPage
}) => {
const requestPromise = comfyPage.page.waitForRequest((request) =>
request.url().includes('/api/models/checkpoints')
)
await addRemoteWidgetNode(comfyPage, 'Remote Widget Node')
const request = await requestPromise
expect(request.url()).toContain('/api/models/checkpoints')
})
})
test.describe('Refresh Behavior', () => {
test('refreshes options when TTL expires', async ({ comfyPage }) => {
// Fulfill each request with a unique timestamp
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route, request) => {
await route.fulfill({
body: JSON.stringify([Date.now()]),
status: 200
})
}
)
const nodeName = 'Remote Widget Node With 300ms Refresh'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
const initialOptions = await getWidgetOptions(comfyPage, nodeName)
// Wait for the refresh (TTL) to expire
await comfyPage.page.waitForTimeout(302)
await comfyPage.page.mouse.click(100, 100)
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
expect(refreshedOptions).not.toEqual(initialOptions)
})
test('does not refresh when TTL is not set', async ({ comfyPage }) => {
let requestCount = 0
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
requestCount++
await route.fulfill({ body: JSON.stringify(['test']), status: 200 })
}
)
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
// Force multiple re-renders
for (let i = 0; i < 3; i++) {
await comfyPage.page.mouse.click(100, 100)
await comfyPage.nextFrame()
}
expect(requestCount).toBe(1) // Should only make initial request
})
test('retries failed requests with backoff', async ({ comfyPage }) => {
const timestamps: number[] = []
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
timestamps.push(Date.now())
await route.fulfill({ status: 500 })
}
)
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
// Wait for a few retries
await comfyPage.page.waitForTimeout(1024)
// Verify exponential backoff between retries
const intervals = timestamps.slice(1).map((t, i) => t - timestamps[i])
expect(intervals[1]).toBeGreaterThan(intervals[0])
})
})
test.describe('Cache Behavior', () => {
test('reuses cached data between widgets with same params', async ({
comfyPage
}) => {
let requestCount = 0
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route) => {
requestCount++
await route.fulfill({
body: JSON.stringify(mockOptions),
status: 200
})
}
)
// Add two widgets with same config
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName, 2)
await waitForWidgetUpdate(comfyPage)
expect(requestCount).toBe(1) // Should reuse cached data
})
})
})

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