mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-21 21:09:00 +00:00
test: additional glsl coverage
- validate max preview size - validate multipass - validate image crossing subgraph boundary
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"id": "ee111111-2222-4333-8444-000000000003",
|
||||
"revision": 0,
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 200],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [1] },
|
||||
{ "name": "MASK", "type": "MASK", "links": null }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "aa999999-8888-4777-a666-555555555557",
|
||||
"pos": [500, 200],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image0",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": null }],
|
||||
"title": "GLSL Subgraph With Image",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [[1, 2, 0, 1, 0, "IMAGE"]],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "aa999999-8888-4777-a666-555555555557",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 1,
|
||||
"lastLinkId": 2,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "GLSL Subgraph With Image",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [50, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [900, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "cc777777-6666-4555-a444-333333333333",
|
||||
"name": "image0",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [2],
|
||||
"pos": { "0": 180, "1": 220 }
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "bb888888-7777-4666-a555-444444444446",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1],
|
||||
"pos": { "0": 920, "1": 220 }
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "GLSLShader",
|
||||
"pos": [400, 180],
|
||||
"size": [460, 320],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"label": "image0",
|
||||
"localized_name": "images.image0",
|
||||
"name": "images.image0",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"localized_name": "fragment_shader",
|
||||
"name": "fragment_shader",
|
||||
"type": "STRING",
|
||||
"widget": { "name": "fragment_shader" },
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "size_mode",
|
||||
"name": "size_mode",
|
||||
"type": "COMFY_DYNAMICCOMBO_V3",
|
||||
"widget": { "name": "size_mode" },
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE0",
|
||||
"name": "IMAGE0",
|
||||
"type": "IMAGE",
|
||||
"links": [1]
|
||||
},
|
||||
{
|
||||
"localized_name": "IMAGE1",
|
||||
"name": "IMAGE1",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"localized_name": "IMAGE2",
|
||||
"name": "IMAGE2",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"localized_name": "IMAGE3",
|
||||
"name": "IMAGE3",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "GLSLShader" },
|
||||
"widgets_values": [
|
||||
"#version 300 es\nprecision highp float;\nuniform sampler2D u_image0;\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\nvoid main() {\n fragColor0 = texture(u_image0, v_texCoord);\n}\n",
|
||||
"from_input"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": 1,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 1,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": { "offset": [0, 0], "scale": 1 }
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -70,27 +70,27 @@ class GLSLShaderNode {
|
||||
) {
|
||||
this.node = comfyPage.vueNodes.getNodeLocator(nodeId)
|
||||
this.previewImage = this.node.locator('img[src^="blob:"]')
|
||||
this.shaderTextbox = this.node.getByRole('textbox', {
|
||||
name: 'fragment_shader'
|
||||
})
|
||||
this.widthInput = this.node
|
||||
.getByLabel('size_mode.width', { exact: true })
|
||||
this.shaderTextbox = comfyPage.vueNodes.getWidgetByName(
|
||||
title,
|
||||
'fragment_shader'
|
||||
)
|
||||
this.widthInput = comfyPage.vueNodes
|
||||
.getWidgetByName(title, 'size_mode.width')
|
||||
.locator('input')
|
||||
this.heightInput = this.node
|
||||
.getByLabel('size_mode.height', { exact: true })
|
||||
this.heightInput = comfyPage.vueNodes
|
||||
.getWidgetByName(title, 'size_mode.height')
|
||||
.locator('input')
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire `execution_start` + `executed` with an image output for this node,
|
||||
* which satisfies the `hasExecutionOutput` gate in `useGLSLPreview`.
|
||||
*/
|
||||
async simulateExecutionOutput(ws: WebSocketRoute) {
|
||||
async simulateExecutionOutput(
|
||||
ws: WebSocketRoute,
|
||||
executionNodeId: string = this.nodeId
|
||||
) {
|
||||
const exec = new ExecutionHelper(this.comfyPage, ws)
|
||||
const jobId = await exec.run()
|
||||
await this.comfyPage.nextFrame()
|
||||
exec.executionStart(jobId)
|
||||
exec.executed(jobId, this.nodeId, {
|
||||
exec.executed(jobId, executionNodeId, {
|
||||
images: [{ filename: 'glsl_test.png', subfolder: '', type: 'output' }]
|
||||
})
|
||||
exec.executionSuccess(jobId)
|
||||
@@ -587,4 +587,153 @@ test.describe('GLSL Shader Preview', { tag: ['@vue-nodes', '@node'] }, () => {
|
||||
await subgraph.expectEveryPixelToBe([255, 0, 0, 255])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('renderer paths', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/glsl_shader_standalone')
|
||||
await comfyPage.vueNodes.waitForNodes(1)
|
||||
})
|
||||
|
||||
test('scales custom resolution down to the max preview dimension', async ({
|
||||
comfyPage,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const ws = await getWebSocket()
|
||||
const glsl = new GLSLShaderNode(comfyPage, GLSL_NODE_ID, GLSL_NODE_TITLE)
|
||||
|
||||
await glsl.selectSizeMode('custom')
|
||||
await expect(glsl.widthInput).toBeVisible()
|
||||
await glsl.widthInput.fill('2048')
|
||||
await glsl.widthInput.blur()
|
||||
await glsl.heightInput.fill('1024')
|
||||
await glsl.heightInput.blur()
|
||||
|
||||
await glsl.simulateExecutionOutput(ws)
|
||||
await glsl.waitForBlobSrc()
|
||||
|
||||
// MAX_PREVIEW_DIMENSION is 1024 → clampResolution scales 2048x1024 to 1024x512.
|
||||
await expect
|
||||
.poll(() => glsl.getPreviewNaturalSize())
|
||||
.toEqual({ width: 1024, height: 512 })
|
||||
})
|
||||
|
||||
test('runs a multi-pass shader via #pragma passes', async ({
|
||||
comfyPage,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const ws = await getWebSocket()
|
||||
const glsl = new GLSLShaderNode(comfyPage, GLSL_NODE_ID, GLSL_NODE_TITLE)
|
||||
|
||||
// Pass 0 writes red to the ping-pong FBO; pass 1 swizzles it to green.
|
||||
// If pass 0 didn't run, u_image0 is the black fallback and output is black.
|
||||
const MULTIPASS_SHADER = [
|
||||
'#version 300 es',
|
||||
'#pragma passes 2',
|
||||
'precision highp float;',
|
||||
'uniform int u_pass;',
|
||||
'uniform sampler2D u_image0;',
|
||||
'in vec2 v_texCoord;',
|
||||
'layout(location = 0) out vec4 fragColor0;',
|
||||
'void main() {',
|
||||
' if (u_pass == 0) {',
|
||||
' fragColor0 = vec4(1.0, 0.0, 0.0, 1.0);',
|
||||
' } else {',
|
||||
' vec4 prev = texture(u_image0, v_texCoord);',
|
||||
' fragColor0 = vec4(prev.g, prev.r, 0.0, 1.0);',
|
||||
' }',
|
||||
'}'
|
||||
].join('\n')
|
||||
|
||||
await glsl.shaderTextbox.fill(MULTIPASS_SHADER)
|
||||
await glsl.simulateExecutionOutput(ws)
|
||||
await glsl.waitForBlobSrc()
|
||||
// RGBA16F → WebP round-trip drifts a couple of levels on saturated channels.
|
||||
await glsl.expectEveryPixelToBe([0, 255, 0, 255], 2)
|
||||
})
|
||||
|
||||
test('revokes the preview blob URL when the GLSL node is removed', async ({
|
||||
comfyPage,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const ws = await getWebSocket()
|
||||
const glsl = new GLSLShaderNode(comfyPage, GLSL_NODE_ID, GLSL_NODE_TITLE)
|
||||
|
||||
await glsl.simulateExecutionOutput(ws)
|
||||
const blobUrl = await glsl.waitForBlobSrc()
|
||||
|
||||
// Sanity check: the blob URL resolves before the node is deleted.
|
||||
await expect(
|
||||
comfyPage.page.evaluate((url) => fetch(url), blobUrl)
|
||||
).resolves.toBeTruthy()
|
||||
|
||||
await comfyPage.vueNodes.deleteNode(GLSL_NODE_ID)
|
||||
await expect(glsl.node).toHaveCount(0)
|
||||
|
||||
// Delete tears down the inner scope → URL.revokeObjectURL → fetch rejects.
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(async (url) => {
|
||||
try {
|
||||
await fetch(url)
|
||||
return 'resolved'
|
||||
} catch {
|
||||
return 'rejected'
|
||||
}
|
||||
}, blobUrl)
|
||||
)
|
||||
.toBe('rejected')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('inner GLSL sampling image through subgraph boundary', () => {
|
||||
// Outer ids come from the workflow JSON; inner ids are reassigned on load.
|
||||
const OUTER_LOAD_IMAGE_NODE_ID = '2'
|
||||
const OUTER_SUBGRAPH_NODE_ID = '1'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'nodes/glsl_shader_subgraph_with_loadimage'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes(2)
|
||||
})
|
||||
|
||||
test('inner GLSL node reads upstream image via subgraph boundary', async ({
|
||||
comfyPage,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const ws = await getWebSocket()
|
||||
|
||||
await dropImageOntoLoadImage(
|
||||
comfyPage,
|
||||
OUTER_LOAD_IMAGE_NODE_ID,
|
||||
'image64x64.webp'
|
||||
)
|
||||
|
||||
// Enter the subgraph so the inner GLSL node mounts its LGraphNode.vue
|
||||
await comfyPage.vueNodes.enterSubgraph(OUTER_SUBGRAPH_NODE_ID)
|
||||
await comfyPage.vueNodes.waitForNodes(1)
|
||||
|
||||
const innerNode = comfyPage.vueNodes
|
||||
.getNodeByTitle(GLSL_NODE_TITLE)
|
||||
.first()
|
||||
await innerNode.waitFor({ state: 'visible' })
|
||||
const innerGlslId = await innerNode.getAttribute('data-node-id')
|
||||
if (!innerGlslId) {
|
||||
throw new Error('inner GLSLShader node is missing data-node-id')
|
||||
}
|
||||
|
||||
const glsl = new GLSLShaderNode(comfyPage, innerGlslId, GLSL_NODE_TITLE)
|
||||
await glsl.simulateExecutionOutput(
|
||||
ws,
|
||||
`${OUTER_SUBGRAPH_NODE_ID}:${innerGlslId}`
|
||||
)
|
||||
|
||||
await expect(glsl.previewImage).toBeVisible()
|
||||
await glsl.waitForBlobSrc()
|
||||
|
||||
await expect
|
||||
.poll(() => glsl.getPreviewNaturalSize())
|
||||
.toEqual({ width: 64, height: 64 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user