Compare commits

...

1 Commits

Author SHA1 Message Date
Glary-Bot
a065087e2f test: add tests for virtual node filtering in API export
Add E2E and unit tests verifying that virtual nodes (Note,
MarkdownNote, Reroute, PrimitiveNode) are excluded from API format
export while preserved in standard workflow format.

Unit tests confirm graphToPrompt correctly filters isVirtualNode
from the output object. E2E tests verify the same through the
browser via getExportedWorkflow({ api: true }).

Includes a new test fixture (note_with_ksampler.json) combining
real and virtual nodes for mixed-workflow assertions.
2026-04-19 00:53:03 +00:00
3 changed files with 233 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
{
"last_node_id": 3,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "KSampler",
"pos": [400, 50],
"size": [315, 262],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{ "name": "model", "type": "MODEL", "link": null },
{ "name": "positive", "type": "CONDITIONING", "link": null },
{ "name": "negative", "type": "CONDITIONING", "link": null },
{ "name": "latent_image", "type": "LATENT", "link": null }
],
"outputs": [
{ "name": "LATENT", "type": "LATENT", "links": [], "slot_index": 0 }
],
"properties": { "Node name for S&R": "KSampler" },
"widgets_values": [42, "fixed", 20, 8, "euler", "normal", 1]
},
{
"id": 2,
"type": "Note",
"pos": [50, 50],
"size": [300, 150],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": ["This is a reference note"],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 3,
"type": "MarkdownNote",
"pos": [50, 250],
"size": [300, 150],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": ["# Markdown heading"],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": { "scale": 1, "offset": [0, 0] }
},
"version": 0.4
}

View File

@@ -0,0 +1,82 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Note Node API Export', { tag: '@node' }, () => {
test('excludes Note and MarkdownNote from API format export', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/note_nodes')
const apiWorkflow = await comfyPage.workflow.getExportedWorkflow({
api: true
})
const classTypes = Object.values(apiWorkflow).map((n) => n.class_type)
expect(classTypes, 'API output should not contain Note').not.toContain(
'Note'
)
expect(
classTypes,
'API output should not contain MarkdownNote'
).not.toContain('MarkdownNote')
expect(
Object.keys(apiWorkflow),
'All-virtual workflow should produce empty API output'
).toHaveLength(0)
})
test('preserves real nodes while filtering virtual ones', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/note_with_ksampler')
const apiWorkflow = await comfyPage.workflow.getExportedWorkflow({
api: true
})
const entries = Object.values(apiWorkflow)
expect(entries, 'Exactly one real node in API output').toHaveLength(1)
expect(entries[0].class_type).toBe('KSampler')
})
test('standard workflow export still includes Note nodes', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/note_nodes')
const workflow = await comfyPage.workflow.getExportedWorkflow()
const noteNodes = workflow.nodes.filter(
(n) => n.type === 'Note' || n.type === 'MarkdownNote'
)
expect(
noteNodes,
'Standard export must preserve both Note and MarkdownNote'
).toHaveLength(2)
})
test('no virtual node types leak through graphToPrompt', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/note_with_ksampler')
const virtualNodeCheck = await comfyPage.page.evaluate(async () => {
const { output } = await window.app!.graphToPrompt()
const virtualTypes = ['Note', 'MarkdownNote', 'Reroute', 'PrimitiveNode']
const leaked: string[] = []
for (const node of Object.values(output)) {
if (virtualTypes.includes(node.class_type)) {
leaked.push(node.class_type)
}
}
return { leaked, totalNodes: Object.keys(output).length }
})
expect(
virtualNodeCheck.leaked,
'No virtual node types should leak into API output'
).toHaveLength(0)
expect(virtualNodeCheck.totalNodes).toBeGreaterThan(0)
})
})

View File

@@ -0,0 +1,88 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { graphToPrompt } from './executionUtil'
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
describe('graphToPrompt', () => {
it('excludes nodes with isVirtualNode from API output', async () => {
const graph = new LGraph()
const realNode = new LGraphNode('RealNode')
realNode.comfyClass = 'KSampler'
graph.add(realNode)
const virtualNode = new LGraphNode('VirtualNode')
virtualNode.isVirtualNode = true
virtualNode.comfyClass = 'Note'
graph.add(virtualNode)
const { output } = await graphToPrompt(graph)
expect(output[String(virtualNode.id)]).toBeUndefined()
expect(output[String(realNode.id)]).toBeDefined()
expect(output[String(realNode.id)].class_type).toBe('KSampler')
})
it('produces empty output when all nodes are virtual', async () => {
const graph = new LGraph()
const note = new LGraphNode('Note')
note.isVirtualNode = true
note.comfyClass = 'Note'
graph.add(note)
const mdNote = new LGraphNode('MarkdownNote')
mdNote.isVirtualNode = true
mdNote.comfyClass = 'MarkdownNote'
graph.add(mdNote)
const { output } = await graphToPrompt(graph)
expect(Object.keys(output)).toHaveLength(0)
})
it('includes virtual nodes in workflow JSON for save fidelity', async () => {
const graph = new LGraph()
const note = new LGraphNode('Note')
note.isVirtualNode = true
note.comfyClass = 'Note'
graph.add(note)
const realNode = new LGraphNode('RealNode')
realNode.comfyClass = 'KSampler'
graph.add(realNode)
const { workflow, output } = await graphToPrompt(graph)
expect(
workflow.nodes.some((n) => n.id === note.id),
'Workflow JSON should preserve virtual nodes by ID'
).toBe(true)
expect(output[String(note.id)]).toBeUndefined()
})
it('preserves multiple non-virtual nodes', async () => {
const graph = new LGraph()
const node1 = new LGraphNode('Node1')
node1.comfyClass = 'KSampler'
graph.add(node1)
const node2 = new LGraphNode('Node2')
node2.comfyClass = 'SaveImage'
graph.add(node2)
const { output } = await graphToPrompt(graph)
expect(Object.keys(output)).toHaveLength(2)
expect(output[String(node1.id)].class_type).toBe('KSampler')
expect(output[String(node2.id)].class_type).toBe('SaveImage')
})
})