mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
Add Playwright tests for async execution
This commit is contained in:
4
.github/workflows/test-ui.yaml
vendored
4
.github/workflows/test-ui.yaml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
|
||||
python main.py --cpu --multi-user --cache-none --front-end-root ../ComfyUI_frontend/dist &
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
run: npx playwright test --project=${{ matrix.browser }}
|
||||
run: npx playwright test --project=${{ matrix.browser }} --workers=1
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -26,6 +26,12 @@ The `.env` file will not exist until you create it yourself.
|
||||
|
||||
A template with helpful information can be found in `.env_example`.
|
||||
|
||||
### Running ComfyUI Backend
|
||||
When running browser tests, ComfyUI must be started with the `--cache-none` argument to disable execution caching:
|
||||
```bash
|
||||
python main.py --cache-none
|
||||
```
|
||||
|
||||
### Multiple Tests
|
||||
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
|
||||
|
||||
|
||||
293
browser_tests/assets/execution/nested-subgraph-test.json
Normal file
293
browser_tests/assets/execution/nested-subgraph-test.json
Normal file
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"id": "test-subgraph-workflow",
|
||||
"revision": 0,
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 200],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [1], "slot_index": 0 },
|
||||
{ "name": "MASK", "type": "MASK", "links": null, "slot_index": 1 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "test-subgraph-1",
|
||||
"pos": [400, 200],
|
||||
"size": [200, 80],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [9] }
|
||||
],
|
||||
"title": "Test Subgraph",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "SaveImage",
|
||||
"pos": [700, 200],
|
||||
"size": [315, 270],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "images", "type": "IMAGE", "link": 9 }
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 10, 0, "IMAGE"],
|
||||
[9, 10, 0, 11, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "test-subgraph-1",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 7,
|
||||
"lastLinkId": 6,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Test Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [-154, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [800, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "input-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": -134,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [5],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": 820,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "TestSleep",
|
||||
"pos": [100, 200],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [2], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.0]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "test-subgraph-2",
|
||||
"pos": [350, 200],
|
||||
"size": [200, 80],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "link": 2 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [5] }
|
||||
],
|
||||
"title": "Nested Test Subgraph",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": 5,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"origin_id": 5,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "test-subgraph-2",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 7,
|
||||
"lastLinkId": 4,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Nested Test Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [-154, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [600, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "input-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": -134,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [4],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": 620,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 6,
|
||||
"type": "TestSleep",
|
||||
"pos": [100, 150],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [2], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.0]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "TestAsyncProgressNode",
|
||||
"pos": [350, 150],
|
||||
"size": [210, 126],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 2 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [4], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestAsyncProgressNode" },
|
||||
"widgets_values": [3.0, 10]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": 7,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
139
browser_tests/assets/execution/parallel_async_nodes.json
Normal file
139
browser_tests/assets/execution/parallel_async_nodes.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [26, 200],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [1, 2, 3], "slot_index": 0 },
|
||||
{ "name": "MASK", "type": "MASK", "links": null, "slot_index": 1 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "TestSleep",
|
||||
"pos": [400, 100],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [4], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.0]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "TestSleep",
|
||||
"pos": [400, 250],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 2 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [5], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.5]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "TestAsyncProgressNode",
|
||||
"pos": [400, 400],
|
||||
"size": [210, 126],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 3 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [6], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestAsyncProgressNode" },
|
||||
"widgets_values": [3.0, 10]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "TestVariadicAverage",
|
||||
"pos": [700, 200],
|
||||
"size": [210, 106],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "input1", "type": "IMAGE", "link": 4 },
|
||||
{ "name": "input2", "type": "IMAGE", "link": 5 },
|
||||
{ "name": "input3", "type": "IMAGE", "link": 6 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [7], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestVariadicAverage" }
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SaveImage",
|
||||
"pos": [1000, 200],
|
||||
"size": [315, 270],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "images", "type": "IMAGE", "link": 7 }
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [700, 400],
|
||||
"size": [210, 54],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "clip", "type": "CLIP", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "CONDITIONING", "type": "CONDITIONING", "links": [], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" },
|
||||
"widgets_values": ["test"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 2, 0, "IMAGE"],
|
||||
[2, 1, 0, 3, 0, "IMAGE"],
|
||||
[3, 1, 0, 4, 0, "IMAGE"],
|
||||
[4, 2, 0, 5, 0, "IMAGE"],
|
||||
[5, 3, 0, 5, 1, "IMAGE"],
|
||||
[6, 4, 0, 5, 2, "IMAGE"],
|
||||
[7, 5, 0, 6, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
193
browser_tests/assets/execution/parallel_ksamplers.json
Normal file
193
browser_tests/assets/execution/parallel_ksamplers.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 13,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [26, 474],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{ "name": "MODEL", "type": "MODEL", "links": [1, 10], "slot_index": 0 },
|
||||
{ "name": "CLIP", "type": "CLIP", "links": [2, 3], "slot_index": 1 },
|
||||
{ "name": "VAE", "type": "VAE", "links": [8, 11], "slot_index": 2 }
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [415, 186],
|
||||
"size": [422.84503173828125, 164.31304931640625],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "clip", "type": "CLIP", "link": 2 }],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [4, 12],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [413, 389],
|
||||
"size": [425.27801513671875, 180.6060791015625],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "clip", "type": "CLIP", "link": 3 }],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [5, 13],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["text, watermark"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [473, 609],
|
||||
"size": [315, 106],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [6, 14], "slot_index": 0 }],
|
||||
"properties": {},
|
||||
"widgets_values": [512, 512, 1]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 186],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": 1 },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": 4 },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": 5 },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": 6 }
|
||||
],
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [7], "slot_index": 0 }],
|
||||
"properties": {},
|
||||
"widgets_values": [156680208700286, true, 2, 8, "euler", "normal", 1]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 188],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "samples", "type": "LATENT", "link": 7 },
|
||||
{ "name": "vae", "type": "VAE", "link": 8 }
|
||||
],
|
||||
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": [9], "slot_index": 0 }],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 189],
|
||||
"size": [210, 26],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "images", "type": "IMAGE", "link": 9 }],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 486],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": 10 },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": 12 },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": 13 },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": 14 }
|
||||
],
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [15], "slot_index": 0 }],
|
||||
"properties": {},
|
||||
"widgets_values": [156680208700287, true, 3, 8, "euler", "normal", 1]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 488],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "samples", "type": "LATENT", "link": 15 },
|
||||
{ "name": "vae", "type": "VAE", "link": 11 }
|
||||
],
|
||||
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": [16], "slot_index": 0 }],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 489],
|
||||
"size": [210, 26],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "images", "type": "IMAGE", "link": 16 }],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 5, 0, "MODEL"],
|
||||
[2, 1, 1, 2, 0, "CLIP"],
|
||||
[3, 1, 1, 3, 0, "CLIP"],
|
||||
[4, 2, 0, 5, 1, "CONDITIONING"],
|
||||
[5, 3, 0, 5, 2, "CONDITIONING"],
|
||||
[6, 4, 0, 5, 3, "LATENT"],
|
||||
[7, 5, 0, 6, 0, "LATENT"],
|
||||
[8, 1, 2, 6, 1, "VAE"],
|
||||
[9, 6, 0, 7, 0, "IMAGE"],
|
||||
[10, 1, 0, 8, 0, "MODEL"],
|
||||
[11, 1, 2, 9, 1, "VAE"],
|
||||
[12, 2, 0, 8, 1, "CONDITIONING"],
|
||||
[13, 3, 0, 8, 2, "CONDITIONING"],
|
||||
[14, 4, 0, 8, 3, "LATENT"],
|
||||
[15, 8, 0, 9, 0, "LATENT"],
|
||||
[16, 9, 0, 10, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
507
browser_tests/helpers/ExecutionTestHelper.ts
Normal file
507
browser_tests/helpers/ExecutionTestHelper.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export interface ExecutionEventTracker {
|
||||
progressStates: any[]
|
||||
executionStarted: boolean
|
||||
executionFinished: boolean
|
||||
executionError: any | null
|
||||
executingNodeId: string | null
|
||||
}
|
||||
|
||||
export interface ProgressState {
|
||||
prompt_id: string
|
||||
nodes: Record<
|
||||
string,
|
||||
{
|
||||
state: 'running' | 'finished' | 'waiting'
|
||||
node_id: string
|
||||
display_node_id: string
|
||||
prompt_id: string
|
||||
value?: number
|
||||
max?: number
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export class ExecutionTestHelper {
|
||||
private testId: string
|
||||
|
||||
constructor(private page: Page) {
|
||||
// Generate unique ID for this test instance to avoid conflicts
|
||||
this.testId = `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up common event tracking for execution tests
|
||||
*/
|
||||
async setupEventTracking(): Promise<void> {
|
||||
await this.page.evaluate((testId) => {
|
||||
// Use unique property names for this test instance
|
||||
const progressKey = `__progressStates_${testId}`
|
||||
const startedKey = `__executionStarted_${testId}`
|
||||
const finishedKey = `__executionFinished_${testId}`
|
||||
const errorKey = `__executionError_${testId}`
|
||||
const nodeIdKey = `__executingNodeId_${testId}`
|
||||
|
||||
window[progressKey] = []
|
||||
window[startedKey] = false
|
||||
window[finishedKey] = false
|
||||
window[errorKey] = null
|
||||
window[nodeIdKey] = null
|
||||
|
||||
const api = window['app'].api
|
||||
|
||||
// Store listeners so we can clean them up later
|
||||
if (!window['__testListeners']) {
|
||||
window['__testListeners'] = {}
|
||||
}
|
||||
|
||||
// Remove old listeners if they exist
|
||||
if (window['__testListeners'][testId]) {
|
||||
const oldListeners = window['__testListeners'][testId]
|
||||
api.removeEventListener('progress_state', oldListeners.progress)
|
||||
api.removeEventListener('executing', oldListeners.executing)
|
||||
api.removeEventListener('execution_error', oldListeners.error)
|
||||
}
|
||||
|
||||
// Create new listeners
|
||||
const progressListener = (event) => {
|
||||
window[progressKey].push(event.detail)
|
||||
}
|
||||
|
||||
const executingListener = (event) => {
|
||||
window[nodeIdKey] = event.detail
|
||||
if (event.detail !== null) {
|
||||
window[startedKey] = true
|
||||
} else {
|
||||
window[finishedKey] = true
|
||||
}
|
||||
}
|
||||
|
||||
const errorListener = (event) => {
|
||||
window[errorKey] = event.detail
|
||||
}
|
||||
|
||||
// Add listeners
|
||||
api.addEventListener('progress_state', progressListener)
|
||||
api.addEventListener('executing', executingListener)
|
||||
api.addEventListener('execution_error', errorListener)
|
||||
|
||||
// Store listeners for cleanup
|
||||
window['__testListeners'][testId] = {
|
||||
progress: progressListener,
|
||||
executing: executingListener,
|
||||
error: errorListener
|
||||
}
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current event tracking state
|
||||
*/
|
||||
async getEventState(): Promise<ExecutionEventTracker> {
|
||||
return await this.page.evaluate(
|
||||
(testId) => ({
|
||||
progressStates: window[`__progressStates_${testId}`] || [],
|
||||
executionStarted: window[`__executionStarted_${testId}`] || false,
|
||||
executionFinished: window[`__executionFinished_${testId}`] || false,
|
||||
executionError: window[`__executionError_${testId}`] || null,
|
||||
executingNodeId: window[`__executingNodeId_${testId}`] || null
|
||||
}),
|
||||
this.testId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for execution to start
|
||||
*/
|
||||
async waitForExecutionStart(timeout: number = 10000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(testId) => window[`__executionStarted_${testId}`] === true,
|
||||
this.testId,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for execution to finish
|
||||
*/
|
||||
async waitForExecutionFinish(timeout: number = 30000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(testId) => window[`__executionFinished_${testId}`] === true,
|
||||
this.testId,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific number of nodes to be running
|
||||
*/
|
||||
async waitForRunningNodes(
|
||||
count: number,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
({ expectedCount, testId }) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running'
|
||||
).length
|
||||
|
||||
return runningNodes >= expectedCount
|
||||
},
|
||||
{ expectedCount: count, testId: this.testId },
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for at least one node to finish
|
||||
*/
|
||||
async waitForNodeFinish(timeout: number = 15000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
return Object.values(latestState.nodes).some(
|
||||
(node: any) => node.state === 'finished'
|
||||
)
|
||||
},
|
||||
this.testId,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest progress state
|
||||
*/
|
||||
async getLatestProgressState(): Promise<ProgressState | null> {
|
||||
return await this.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
return states[states.length - 1]
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for node progress to be applied to the graph
|
||||
*/
|
||||
async waitForGraphNodeProgress(
|
||||
nodeIds: number[],
|
||||
timeout: number = 5000
|
||||
): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(ids) => {
|
||||
return ids.some((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node?.progress !== undefined && node.progress >= 0
|
||||
})
|
||||
},
|
||||
nodeIds,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets node progress from the graph
|
||||
*/
|
||||
async getGraphNodeProgress(nodeId: number): Promise<number | undefined> {
|
||||
return await this.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node?.progress
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if execution had errors
|
||||
*/
|
||||
async hasExecutionError(): Promise<boolean> {
|
||||
const error = await this.page.evaluate(
|
||||
(testId) => window[`__executionError_${testId}`],
|
||||
this.testId
|
||||
)
|
||||
return error !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets execution error details
|
||||
*/
|
||||
async getExecutionError(): Promise<any> {
|
||||
return await this.page.evaluate(
|
||||
(testId) => window[`__executionError_${testId}`],
|
||||
this.testId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup event listeners when test is done
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
await this.page.evaluate((testId) => {
|
||||
if (window['__testListeners'] && window['__testListeners'][testId]) {
|
||||
const api = window['app'].api
|
||||
const listeners = window['__testListeners'][testId]
|
||||
api.removeEventListener('progress_state', listeners.progress)
|
||||
api.removeEventListener('executing', listeners.executing)
|
||||
api.removeEventListener('execution_error', listeners.error)
|
||||
delete window['__testListeners'][testId]
|
||||
}
|
||||
// Clean up test-specific properties
|
||||
delete window[`__progressStates_${testId}`]
|
||||
delete window[`__executionStarted_${testId}`]
|
||||
delete window[`__executionFinished_${testId}`]
|
||||
delete window[`__executionError_${testId}`]
|
||||
delete window[`__executingNodeId_${testId}`]
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the testId for direct window access in evaluate functions
|
||||
*/
|
||||
getTestId(): string {
|
||||
return this.testId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for browser title monitoring
|
||||
*/
|
||||
export class BrowserTitleMonitor {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Waits for title to not show execution state
|
||||
*/
|
||||
async waitForIdleTitle(timeout: number = 10000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
return !title.match(/\[\d+%\]/) && !title.match(/\[\d+ nodes running\]/)
|
||||
},
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for title to show execution state
|
||||
*/
|
||||
async waitForExecutionTitle(timeout: number = 5000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
return title.match(/\[\d+%\]/) || title.match(/\[\d+ nodes running\]/)
|
||||
},
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up title change monitoring
|
||||
*/
|
||||
async setupTitleMonitoring(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
window['__titleUpdateLog'] = []
|
||||
window['__lastTitle'] = document.title
|
||||
|
||||
window['__titleInterval'] = setInterval(() => {
|
||||
const newTitle = document.title
|
||||
if (newTitle !== window['__lastTitle']) {
|
||||
window['__titleUpdateLog'].push({
|
||||
time: Date.now(),
|
||||
title: newTitle,
|
||||
hasProgress: !!newTitle.match(/\[\d+%\]/),
|
||||
hasMultiNode: !!newTitle.match(/\[\d+ nodes running\]/)
|
||||
})
|
||||
window['__lastTitle'] = newTitle
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops title monitoring and returns the log
|
||||
*/
|
||||
async stopTitleMonitoring(): Promise<any[]> {
|
||||
const log = await this.page.evaluate(() => {
|
||||
if (window['__titleInterval']) {
|
||||
clearInterval(window['__titleInterval'])
|
||||
}
|
||||
return window['__titleUpdateLog'] || []
|
||||
})
|
||||
return log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for preview event handling
|
||||
*/
|
||||
export class PreviewTestHelper {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Sets up preview event tracking
|
||||
*/
|
||||
async setupPreviewTracking(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
window['__previewEvents'] = []
|
||||
window['__revokedNodes'] = []
|
||||
window['__revokedUrls'] = []
|
||||
|
||||
// Track preview events
|
||||
const api = window['app'].api
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
window['__previewEvents'].push({
|
||||
nodeId: event.detail.nodeId,
|
||||
displayNodeId: event.detail.displayNodeId,
|
||||
parentNodeId: event.detail.parentNodeId,
|
||||
realNodeId: event.detail.realNodeId,
|
||||
promptId: event.detail.promptId
|
||||
})
|
||||
})
|
||||
|
||||
// Mock revokePreviews to track calls
|
||||
const originalRevoke = window['app'].revokePreviews
|
||||
window['app'].revokePreviews = function (nodeId) {
|
||||
window['__revokedNodes'].push(nodeId)
|
||||
originalRevoke.call(this, nodeId)
|
||||
}
|
||||
|
||||
// Mock URL.revokeObjectURL to track URL revocations
|
||||
const originalRevokeURL = URL.revokeObjectURL
|
||||
URL.revokeObjectURL = (url: string) => {
|
||||
window['__revokedUrls'].push(url)
|
||||
originalRevokeURL.call(URL, url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tracked preview events
|
||||
*/
|
||||
async getPreviewEvents(): Promise<any[]> {
|
||||
return await this.page.evaluate(() => window['__previewEvents'] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets revoked node IDs
|
||||
*/
|
||||
async getRevokedNodes(): Promise<string[]> {
|
||||
return await this.page.evaluate(() => window['__revokedNodes'] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets revoked URLs
|
||||
*/
|
||||
async getRevokedUrls(): Promise<string[]> {
|
||||
return await this.page.evaluate(() => window['__revokedUrls'] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets fake preview for a node
|
||||
*/
|
||||
async setNodePreview(nodeId: string, previewUrl: string): Promise<void> {
|
||||
await this.page.evaluate(
|
||||
({ id, url }) => {
|
||||
window['app'].nodePreviewImages[id] = [url]
|
||||
},
|
||||
{ id: nodeId, url: previewUrl }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets node preview URLs
|
||||
*/
|
||||
async getNodePreviews(nodeId: string): Promise<string[] | undefined> {
|
||||
return await this.page.evaluate(
|
||||
(id) => window['app'].nodePreviewImages[id],
|
||||
nodeId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for checking subgraph execution
|
||||
*/
|
||||
export class SubgraphTestHelper {
|
||||
private testId: string
|
||||
|
||||
constructor(private page: Page) {
|
||||
// Generate unique ID for this test instance
|
||||
this.testId = `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the test ID to match ExecutionTestHelper
|
||||
*/
|
||||
setTestId(testId: string): void {
|
||||
this.testId = testId
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for nested node progress (nodes with ':' in their ID)
|
||||
*/
|
||||
async waitForNestedNodeProgress(
|
||||
minNestingLevel: number = 1,
|
||||
timeout: number = 15000
|
||||
): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
({ minLevel, testId }) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
return states.some((state: any) => {
|
||||
if (!state.nodes) return false
|
||||
return Object.keys(state.nodes).some((nodeId) => {
|
||||
const colonCount = (nodeId.match(/:/g) || []).length
|
||||
return colonCount >= minLevel
|
||||
})
|
||||
})
|
||||
},
|
||||
{ minLevel: minNestingLevel, testId: this.testId },
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all nested node IDs from progress states
|
||||
*/
|
||||
async getNestedNodeIds(): Promise<string[]> {
|
||||
return await this.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`] || []
|
||||
const nestedIds = new Set<string>()
|
||||
|
||||
states.forEach((state: any) => {
|
||||
if (state.nodes) {
|
||||
Object.keys(state.nodes).forEach((nodeId) => {
|
||||
if (nodeId.includes(':')) {
|
||||
nestedIds.add(nodeId)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(nestedIds)
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node has running stroke style
|
||||
*/
|
||||
async hasRunningStrokeStyle(nodeId: number): Promise<boolean> {
|
||||
return await this.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node?.strokeStyles?.['running']) return false
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color === '#0f0'
|
||||
}, nodeId)
|
||||
}
|
||||
}
|
||||
316
browser_tests/tests/browserTabTitleMultiNode.spec.ts
Normal file
316
browser_tests/tests/browserTabTitleMultiNode.spec.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
BrowserTitleMonitor,
|
||||
ExecutionTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Browser Tab Title - Multi-node Execution', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let titleMonitor: BrowserTitleMonitor
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
titleMonitor = new BrowserTitleMonitor(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
// Clean up event listeners to avoid conflicts
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Shows multiple nodes running in tab title', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle()
|
||||
|
||||
// Get initial title
|
||||
const initialTitle = await comfyPage.page.title()
|
||||
// Title might show execution state if other tests are running
|
||||
// Just ensure we have a baseline to compare against
|
||||
|
||||
// Wait for the UI to be ready
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check if workflow is valid and nodes are available
|
||||
const workflowStatus = await comfyPage.page.evaluate(() => {
|
||||
const graph = window['app'].graph
|
||||
const missingNodeTypes: string[] = []
|
||||
const nodeCount = graph.nodes.length
|
||||
|
||||
// Check for missing node types
|
||||
graph.nodes.forEach((node: any) => {
|
||||
if (node.type && !LiteGraph.registered_node_types[node.type]) {
|
||||
missingNodeTypes.push(node.type)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
nodeCount,
|
||||
missingNodeTypes,
|
||||
hasErrors: missingNodeTypes.length > 0
|
||||
}
|
||||
})
|
||||
|
||||
if (workflowStatus.hasErrors) {
|
||||
console.log('Missing node types:', workflowStatus.missingNodeTypes)
|
||||
// Skip test if nodes are missing
|
||||
test.skip()
|
||||
return
|
||||
}
|
||||
|
||||
// Set up tracking for progress events and errors
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Queue the workflow for real execution using the command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait a moment to see if there's an error
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Check for execution errors
|
||||
if (await executionHelper.hasExecutionError()) {
|
||||
const error = await executionHelper.getExecutionError()
|
||||
console.log('Execution error:', error)
|
||||
}
|
||||
|
||||
// Wait for multiple nodes to be running (TestSleep nodes 2, 3 and TestAsyncProgressNode 4)
|
||||
await executionHelper.waitForRunningNodes(2)
|
||||
|
||||
// Check title while we know multiple nodes are running
|
||||
const testId = executionHelper.getTestId()
|
||||
const titleDuringExecution = await comfyPage.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return null
|
||||
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running'
|
||||
).length
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
runningCount: runningNodes
|
||||
}
|
||||
}, testId)
|
||||
|
||||
// Verify we captured the state with multiple nodes running
|
||||
expect(titleDuringExecution).not.toBeNull()
|
||||
expect(titleDuringExecution.runningCount).toBeGreaterThanOrEqual(2)
|
||||
|
||||
// The title should show multiple nodes running when we have 2+ nodes executing
|
||||
if (titleDuringExecution.runningCount >= 2) {
|
||||
expect(titleDuringExecution.title).toMatch(/\[\d+ nodes running\]/)
|
||||
}
|
||||
|
||||
// Wait for some nodes to finish, leaving only one running
|
||||
await executionHelper.waitForRunningNodes(1, 15000)
|
||||
|
||||
// Wait for title to show single node progress
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
return title.match(/\[\d+%\]/) && !title.match(/\[\d+ nodes running\]/)
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that title shows single node with progress
|
||||
const titleWithSingleNode = await comfyPage.page.title()
|
||||
expect(titleWithSingleNode).toMatch(/\[\d+%\]/)
|
||||
expect(titleWithSingleNode).not.toMatch(/\[\d+ nodes running\]/)
|
||||
})
|
||||
|
||||
test('Shows progress updates in title during execution', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for the UI to be ready
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Set up tracking for progress events and title changes
|
||||
await executionHelper.setupEventTracking()
|
||||
await titleMonitor.setupTitleMonitoring()
|
||||
|
||||
// Queue the workflow for real execution using the command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for TestAsyncProgressNode (node 4) to start showing progress
|
||||
// This node reports progress from 0 to 10 with steps of 1
|
||||
const testId2 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes || !latestState.nodes['4']) return false
|
||||
|
||||
const node4 = latestState.nodes['4']
|
||||
if (node4.state === 'running' && node4.value > 0) {
|
||||
window['__lastProgress'] = Math.round((node4.value / node4.max) * 100)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
testId2,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for title to show progress percentage
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
console.log('Title check 1:', title)
|
||||
return title.match(/\[\d+%\]/)
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that title shows a progress percentage
|
||||
const titleWithProgress = await comfyPage.page.title()
|
||||
expect(titleWithProgress).toMatch(/\[\d+%\]/)
|
||||
|
||||
// Wait for progress to update to a different value
|
||||
const firstProgress = await comfyPage.page.evaluate(
|
||||
() => window['__lastProgress']
|
||||
)
|
||||
|
||||
const testId3 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
({ initialProgress, testId }) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes || !latestState.nodes['4']) return false
|
||||
|
||||
const node4 = latestState.nodes['4']
|
||||
if (node4.state === 'running') {
|
||||
const currentProgress = Math.round((node4.value / node4.max) * 100)
|
||||
window['__lastProgress'] = currentProgress
|
||||
return currentProgress > initialProgress
|
||||
}
|
||||
return false
|
||||
},
|
||||
{ initialProgress: firstProgress, testId: testId3 },
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Store the first progress for comparison
|
||||
await comfyPage.page.evaluate((progress) => {
|
||||
window['__firstProgress'] = progress
|
||||
}, firstProgress)
|
||||
|
||||
// Check the title history to verify we captured progress updates
|
||||
const finalCheck = await comfyPage.page.evaluate(() => {
|
||||
const titleLog = window['__titleUpdateLog'] || []
|
||||
const firstProgress = window['__firstProgress'] || 0
|
||||
|
||||
// Find titles with progress information
|
||||
const titlesWithProgress = titleLog.filter((entry) => entry.hasProgress)
|
||||
|
||||
// Check if we saw different progress values or multi-node running state
|
||||
const progressValues = new Set()
|
||||
const hadMultiNodeRunning = titleLog.some((entry) =>
|
||||
entry.title.includes('nodes running')
|
||||
)
|
||||
|
||||
titleLog.forEach((entry) => {
|
||||
const match = entry.title.match(/\[(\d+)%\]/)
|
||||
if (match) {
|
||||
progressValues.add(parseInt(match[1]))
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
sawProgressUpdates: titlesWithProgress.length > 0,
|
||||
uniqueProgressValues: Array.from(progressValues),
|
||||
hadMultiNodeRunning,
|
||||
firstProgress,
|
||||
lastProgress: window['__lastProgress'],
|
||||
totalTitleUpdates: titleLog.length,
|
||||
sampleTitles: titleLog.slice(0, 5)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Title update check:', JSON.stringify(finalCheck, null, 2))
|
||||
|
||||
// Verify that we captured title updates showing execution progress
|
||||
expect(finalCheck.sawProgressUpdates).toBe(true)
|
||||
expect(finalCheck.totalTitleUpdates).toBeGreaterThan(0)
|
||||
|
||||
// We should have seen either:
|
||||
// 1. Multiple unique progress values, OR
|
||||
// 2. Multi-node running state, OR
|
||||
// 3. Progress different from initial
|
||||
const sawProgressChange =
|
||||
finalCheck.uniqueProgressValues.length > 1 ||
|
||||
finalCheck.hadMultiNodeRunning ||
|
||||
finalCheck.lastProgress !== firstProgress
|
||||
|
||||
expect(sawProgressChange).toBe(true)
|
||||
|
||||
// Clean up interval
|
||||
await titleMonitor.stopTitleMonitoring()
|
||||
})
|
||||
|
||||
test('Clears execution status from title when all nodes finish', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle()
|
||||
|
||||
// Wait for the UI to be ready
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Set up tracking for events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Queue the workflow for real execution using the command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to show progress in title
|
||||
await titleMonitor.waitForExecutionTitle()
|
||||
|
||||
// Verify execution shows in title
|
||||
const executingTitle = await comfyPage.page.title()
|
||||
expect(executingTitle).toMatch(/\[[\d%\s\w]+\]/)
|
||||
|
||||
// Wait for execution to complete (all nodes finished)
|
||||
await executionHelper.waitForExecutionFinish()
|
||||
|
||||
// Give a moment for title to update after execution completes
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Wait for title to clear execution status
|
||||
await titleMonitor.waitForIdleTitle()
|
||||
|
||||
// Check that execution status is cleared
|
||||
const finishedTitle = await comfyPage.page.title()
|
||||
expect(finishedTitle).toContain('ComfyUI')
|
||||
expect(finishedTitle).not.toMatch(/\[\d+%\]/) // No percentage
|
||||
expect(finishedTitle).not.toMatch(/\[\d+ nodes running\]/) // No running nodes
|
||||
expect(finishedTitle).not.toContain('Executing')
|
||||
})
|
||||
})
|
||||
85
browser_tests/tests/browserTabTitleSimple.spec.ts
Normal file
85
browser_tests/tests/browserTabTitleSimple.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
BrowserTitleMonitor,
|
||||
ExecutionTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
test.describe('Browser Tab Title - Multi-node Simple', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let titleMonitor: BrowserTitleMonitor
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
titleMonitor = new BrowserTitleMonitor(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Title updates based on execution state', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle().catch(() => {
|
||||
// If timeout, cancel any running execution
|
||||
return comfyPage.page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
// Get initial title
|
||||
const initialTitle = await comfyPage.page.title()
|
||||
// Title might show execution state if other tests are running
|
||||
// Just ensure we can detect when it changes
|
||||
const hasExecutionState =
|
||||
initialTitle.match(/\[\d+%\]/) ||
|
||||
initialTitle.match(/\[\d+ nodes running\]/)
|
||||
|
||||
// Set up tracking for execution events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command instead of button
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for title to update with execution state
|
||||
await titleMonitor.waitForExecutionTitle()
|
||||
|
||||
const executingTitle = await comfyPage.page.title()
|
||||
// If initial title didn't have execution state, it should be different now
|
||||
if (!hasExecutionState) {
|
||||
expect(executingTitle).not.toBe(initialTitle)
|
||||
}
|
||||
expect(executingTitle).toMatch(/\[[\d%\s\w]+\]/)
|
||||
})
|
||||
|
||||
test('Can read workflow name from title', async ({ comfyPage }) => {
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle(5000).catch(async () => {
|
||||
// Cancel any running execution
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
})
|
||||
|
||||
// Set a workflow name
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.workflow.activeWorkflow.filename =
|
||||
'test-workflow'
|
||||
})
|
||||
|
||||
// Wait for title to update
|
||||
await comfyPage.page.waitForTimeout(100)
|
||||
|
||||
const title = await comfyPage.page.title()
|
||||
expect(title).toContain('test-workflow')
|
||||
// Title should contain workflow name regardless of execution state
|
||||
})
|
||||
})
|
||||
385
browser_tests/tests/multiNodeExecution.spec.ts
Normal file
385
browser_tests/tests/multiNodeExecution.spec.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
PreviewTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Multi-node Execution Progress', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let previewHelper: PreviewTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
previewHelper = new PreviewTestHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Can track progress of multiple async nodes executing in parallel', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Get references to the async nodes
|
||||
const sleepNode1 = await comfyPage.getNodeRefById(2)
|
||||
const sleepNode2 = await comfyPage.getNodeRefById(3)
|
||||
const progressNode = await comfyPage.getNodeRefById(4)
|
||||
|
||||
// Verify nodes are present
|
||||
expect(sleepNode1).toBeDefined()
|
||||
expect(sleepNode2).toBeDefined()
|
||||
expect(progressNode).toBeDefined()
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for all three nodes (2, 3, 4) to show progress from real execution
|
||||
const testId = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
const node2 = latestState.nodes['2']
|
||||
const node3 = latestState.nodes['3']
|
||||
const node4 = latestState.nodes['4']
|
||||
|
||||
// Check that all nodes have started executing
|
||||
return (
|
||||
node2 &&
|
||||
node2.state === 'running' &&
|
||||
node3 &&
|
||||
node3.state === 'running' &&
|
||||
node4 &&
|
||||
node4.state === 'running'
|
||||
)
|
||||
},
|
||||
testId,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for progress to be applied to all nodes in the graph
|
||||
await executionHelper.waitForGraphNodeProgress([2, 3, 4])
|
||||
|
||||
// Check that all nodes show progress
|
||||
const nodeProgress1 = await sleepNode1.getProperty('progress')
|
||||
const nodeProgress2 = await sleepNode2.getProperty('progress')
|
||||
const nodeProgress3 = await progressNode.getProperty('progress')
|
||||
|
||||
// Progress values should now be defined (exact values depend on timing)
|
||||
expect(nodeProgress1).toBeDefined()
|
||||
expect(nodeProgress2).toBeDefined()
|
||||
expect(nodeProgress3).toBeDefined()
|
||||
expect(nodeProgress1).toBeGreaterThanOrEqual(0)
|
||||
expect(nodeProgress1).toBeLessThanOrEqual(1)
|
||||
expect(nodeProgress2).toBeGreaterThanOrEqual(0)
|
||||
expect(nodeProgress2).toBeLessThanOrEqual(1)
|
||||
expect(nodeProgress3).toBeGreaterThanOrEqual(0)
|
||||
expect(nodeProgress3).toBeLessThanOrEqual(1)
|
||||
|
||||
// Wait for at least one node to finish
|
||||
await executionHelper.waitForNodeFinish()
|
||||
|
||||
// Wait for the finished node's progress to be cleared
|
||||
const testId2 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
// Find which nodes are finished
|
||||
const finishedNodeIds = Object.entries(latestState.nodes)
|
||||
.filter(([_, node]: [string, any]) => node.state === 'finished')
|
||||
.map(([id, _]) => id)
|
||||
|
||||
// Check that finished nodes have no progress in the graph
|
||||
return finishedNodeIds.some((id) => {
|
||||
const node = window['app'].graph.getNodeById(parseInt(id))
|
||||
return node && node.progress === undefined
|
||||
})
|
||||
},
|
||||
testId2,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Get current state of nodes
|
||||
const testId3 = executionHelper.getTestId()
|
||||
const currentState = await comfyPage.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
const latestState = states[states.length - 1]
|
||||
const graphNodes = {
|
||||
'2': window['app'].graph.getNodeById(2),
|
||||
'3': window['app'].graph.getNodeById(3),
|
||||
'4': window['app'].graph.getNodeById(4)
|
||||
}
|
||||
|
||||
return {
|
||||
stateNodes: latestState.nodes,
|
||||
graphProgress: {
|
||||
'2': graphNodes['2']?.progress,
|
||||
'3': graphNodes['3']?.progress,
|
||||
'4': graphNodes['4']?.progress
|
||||
}
|
||||
}
|
||||
}, testId3)
|
||||
|
||||
// Verify that finished nodes have no progress, running nodes have progress
|
||||
if (currentState && currentState.stateNodes) {
|
||||
Object.entries(currentState.stateNodes).forEach(
|
||||
([nodeId, nodeState]: [string, any]) => {
|
||||
const graphProgress = currentState.graphProgress[nodeId]
|
||||
if (nodeState.state === 'finished') {
|
||||
expect(graphProgress).toBeUndefined()
|
||||
} else if (nodeState.state === 'running') {
|
||||
expect(graphProgress).toBeDefined()
|
||||
expect(graphProgress).toBeGreaterThanOrEqual(0)
|
||||
expect(graphProgress).toBeLessThanOrEqual(1)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up by canceling execution
|
||||
})
|
||||
|
||||
test('Updates visual state for multiple executing nodes', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for the graph to be properly initialized
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
return window['app']?.graph?.nodes?.length > 0
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for multiple nodes to start executing
|
||||
await executionHelper.waitForRunningNodes(2)
|
||||
|
||||
// Wait for the progress to be applied to nodes
|
||||
await executionHelper.waitForGraphNodeProgress([2, 3])
|
||||
|
||||
// Verify that nodes have progress set (indicates they are executing)
|
||||
const nodeStates = await comfyPage.page.evaluate(() => {
|
||||
const node2 = window['app'].graph.getNodeById(2)
|
||||
const node3 = window['app'].graph.getNodeById(3)
|
||||
return {
|
||||
node2Progress: node2?.progress,
|
||||
node3Progress: node3?.progress,
|
||||
// Check if any nodes are marked as running by having progress
|
||||
hasRunningNodes:
|
||||
(node2?.progress !== undefined && node2?.progress >= 0) ||
|
||||
(node3?.progress !== undefined && node3?.progress >= 0)
|
||||
}
|
||||
})
|
||||
|
||||
expect(nodeStates.node2Progress).toBeDefined()
|
||||
expect(nodeStates.node3Progress).toBeDefined()
|
||||
expect(nodeStates.hasRunningNodes).toBe(true)
|
||||
|
||||
// Wait for at least one node to finish
|
||||
await executionHelper.waitForNodeFinish()
|
||||
|
||||
// Wait for progress updates to reflect the finished state
|
||||
const testId4 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
// Find nodes by their state
|
||||
const finishedNodes = Object.entries(latestState.nodes)
|
||||
.filter(([_, node]: [string, any]) => node.state === 'finished')
|
||||
.map(([id, _]) => parseInt(id))
|
||||
|
||||
const runningNodes = Object.entries(latestState.nodes)
|
||||
.filter(([_, node]: [string, any]) => node.state === 'running')
|
||||
.map(([id, _]) => parseInt(id))
|
||||
|
||||
// Check graph nodes match the state
|
||||
const allFinishedCorrect = finishedNodes.every((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node && node.progress === undefined
|
||||
})
|
||||
|
||||
const allRunningCorrect = runningNodes.every((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node && node.progress !== undefined && node.progress >= 0
|
||||
})
|
||||
|
||||
return (
|
||||
allFinishedCorrect && allRunningCorrect && finishedNodes.length > 0
|
||||
)
|
||||
},
|
||||
testId4,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Verify the final node states
|
||||
const testId5 = executionHelper.getTestId()
|
||||
const finalNodeStates = await comfyPage.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
const latestState = states[states.length - 1]
|
||||
const node2 = window['app'].graph.getNodeById(2)
|
||||
const node3 = window['app'].graph.getNodeById(3)
|
||||
|
||||
return {
|
||||
node2State: latestState.nodes['2']?.state,
|
||||
node3State: latestState.nodes['3']?.state,
|
||||
node2Progress: node2?.progress,
|
||||
node3Progress: node3?.progress
|
||||
}
|
||||
}, testId5)
|
||||
|
||||
// Verify finished nodes have no progress, running nodes have progress
|
||||
if (finalNodeStates) {
|
||||
if (finalNodeStates.node2State === 'finished') {
|
||||
expect(finalNodeStates.node2Progress).toBeUndefined()
|
||||
} else if (finalNodeStates.node2State === 'running') {
|
||||
expect(finalNodeStates.node2Progress).toBeDefined()
|
||||
}
|
||||
|
||||
if (finalNodeStates.node3State === 'finished') {
|
||||
expect(finalNodeStates.node3Progress).toBeUndefined()
|
||||
} else if (finalNodeStates.node3State === 'running') {
|
||||
expect(finalNodeStates.node3Progress).toBeDefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Clears previews when nodes start executing', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Initialize tracking for revoked previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['__revokedNodes'] = []
|
||||
})
|
||||
|
||||
// Set up some fake previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].nodePreviewImages['2'] = ['fake-preview-url-1']
|
||||
window['app'].nodePreviewImages['3'] = ['fake-preview-url-2']
|
||||
})
|
||||
|
||||
// Verify previews exist
|
||||
const previewsBefore = await comfyPage.page.evaluate(() => {
|
||||
return {
|
||||
node2: window['app'].nodePreviewImages['2'],
|
||||
node3: window['app'].nodePreviewImages['3']
|
||||
}
|
||||
})
|
||||
|
||||
expect(previewsBefore.node2).toEqual(['fake-preview-url-1'])
|
||||
expect(previewsBefore.node3).toEqual(['fake-preview-url-2'])
|
||||
|
||||
// Mock revokePreviews to track calls and set up event listeners
|
||||
await previewHelper.setupPreviewTracking()
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real execution to trigger progress events that clear previews
|
||||
const testId6 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
// Check if we have progress for nodes 2 and 3
|
||||
const hasNode2Progress = states.some(
|
||||
(state: any) =>
|
||||
state.nodes &&
|
||||
state.nodes['2'] &&
|
||||
state.nodes['2'].state === 'running'
|
||||
)
|
||||
const hasNode3Progress = states.some(
|
||||
(state: any) =>
|
||||
state.nodes &&
|
||||
state.nodes['3'] &&
|
||||
state.nodes['3'].state === 'running'
|
||||
)
|
||||
|
||||
return hasNode2Progress && hasNode3Progress
|
||||
},
|
||||
testId6,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for the event to be processed and previews to be revoked
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const revokedNodes = window['__revokedNodes']
|
||||
const node2PreviewCleared =
|
||||
window['app'].nodePreviewImages['2'] === undefined
|
||||
const node3PreviewCleared =
|
||||
window['app'].nodePreviewImages['3'] === undefined
|
||||
|
||||
return (
|
||||
revokedNodes.includes('2') &&
|
||||
revokedNodes.includes('3') &&
|
||||
node2PreviewCleared &&
|
||||
node3PreviewCleared
|
||||
)
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that revokePreviews was called for both nodes
|
||||
const revokedNodes = await previewHelper.getRevokedNodes()
|
||||
expect(revokedNodes).toContain('2')
|
||||
expect(revokedNodes).toContain('3')
|
||||
|
||||
// Check that previews were cleared
|
||||
const previewsAfter = await comfyPage.page.evaluate(() => {
|
||||
return {
|
||||
node2: window['app'].nodePreviewImages['2'],
|
||||
node3: window['app'].nodePreviewImages['3']
|
||||
}
|
||||
})
|
||||
|
||||
expect(previewsAfter.node2).toBeUndefined()
|
||||
expect(previewsAfter.node3).toBeUndefined()
|
||||
})
|
||||
})
|
||||
168
browser_tests/tests/multiNodeExecutionSimple.spec.ts
Normal file
168
browser_tests/tests/multiNodeExecutionSimple.spec.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
PreviewTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Multi-node Execution Progress - Simple', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let previewHelper: PreviewTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
previewHelper = new PreviewTestHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Can dispatch and receive progress_state events', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Set up event tracking
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for real progress_state events from backend
|
||||
const testId = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
return latestState.nodes && Object.keys(latestState.nodes).length > 0
|
||||
},
|
||||
testId,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Get the captured states
|
||||
const eventState = await executionHelper.getEventState()
|
||||
const result = eventState.progressStates
|
||||
|
||||
// Should have captured real events
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
const firstState = result[0]
|
||||
expect(firstState).toBeDefined()
|
||||
expect(firstState.prompt_id).toBeDefined()
|
||||
expect(firstState.nodes).toBeDefined()
|
||||
|
||||
// Check that we got real node progress
|
||||
const nodeIds = Object.keys(firstState.nodes)
|
||||
expect(nodeIds.length).toBeGreaterThan(0)
|
||||
|
||||
// Verify node structure
|
||||
for (const nodeId of nodeIds) {
|
||||
const node = firstState.nodes[nodeId]
|
||||
expect(node.state).toBeDefined()
|
||||
expect(node.node_id).toBeDefined()
|
||||
expect(node.display_node_id).toBeDefined()
|
||||
expect(node.prompt_id).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
test('Canvas updates when nodes have progress', async ({ comfyPage, ws }) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Set up progress tracking
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for nodes to have progress from real execution
|
||||
const testId2 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
// Check if any nodes are running with progress
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running' && node.value > 0
|
||||
)
|
||||
|
||||
return runningNodes.length > 0
|
||||
},
|
||||
testId2,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for progress to be applied to graph nodes
|
||||
await executionHelper.waitForGraphNodeProgress([2, 3, 4])
|
||||
|
||||
// Check that nodes have progress set from real execution
|
||||
const node2Progress = await executionHelper.getGraphNodeProgress(2)
|
||||
const node3Progress = await executionHelper.getGraphNodeProgress(3)
|
||||
const node4Progress = await executionHelper.getGraphNodeProgress(4)
|
||||
|
||||
// At least one node should have progress
|
||||
const hasProgress =
|
||||
(node2Progress !== undefined && node2Progress > 0) ||
|
||||
(node3Progress !== undefined && node3Progress > 0) ||
|
||||
(node4Progress !== undefined && node4Progress > 0)
|
||||
|
||||
expect(hasProgress).toBe(true)
|
||||
})
|
||||
|
||||
test('Preview events include metadata', async ({ comfyPage, ws }) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Track preview events
|
||||
await previewHelper.setupPreviewTracking()
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// For this test, we'll check the event structure by simulating one
|
||||
// since real preview events depend on the workflow actually generating images
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
// Simulate a preview event that would come from backend
|
||||
api.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob: new Blob(['test'], { type: 'image/png' }),
|
||||
nodeId: '10:5:3',
|
||||
displayNodeId: '10',
|
||||
parentNodeId: '10:5',
|
||||
realNodeId: '3',
|
||||
promptId: 'test-prompt-id'
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check captured events
|
||||
const captured = await previewHelper.getPreviewEvents()
|
||||
expect(captured).toHaveLength(1)
|
||||
expect(captured[0]).toEqual({
|
||||
nodeId: '10:5:3',
|
||||
displayNodeId: '10',
|
||||
parentNodeId: '10:5',
|
||||
realNodeId: '3',
|
||||
promptId: 'test-prompt-id'
|
||||
})
|
||||
})
|
||||
})
|
||||
272
browser_tests/tests/previewWithMetadata.spec.ts
Normal file
272
browser_tests/tests/previewWithMetadata.spec.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
PreviewTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Preview with Metadata', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let previewHelper: PreviewTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
previewHelper = new PreviewTestHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Handles b_preview_with_metadata event correctly', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Clear any existing previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].nodePreviewImages = {}
|
||||
})
|
||||
|
||||
// Set up handler to track preview events and execution
|
||||
await executionHelper.setupEventTracking()
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['__previewHandled'] = false
|
||||
const api = window['app'].api
|
||||
|
||||
// Add handler to track preview events
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
const { displayNodeId, blob } = event.detail
|
||||
// Create URL from the blob in the event
|
||||
const url = URL.createObjectURL(blob)
|
||||
window['app'].nodePreviewImages[displayNodeId] = [url]
|
||||
window['__previewHandled'] = true
|
||||
window['__lastPreviewUrl'] = url
|
||||
})
|
||||
})
|
||||
|
||||
// Start real execution to test event handling in context
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Trigger b_preview_with_metadata event (simulating what backend would send)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const event = new CustomEvent('b_preview_with_metadata', {
|
||||
detail: {
|
||||
blob: new Blob(['test'], { type: 'image/png' }),
|
||||
nodeId: '2',
|
||||
displayNodeId: '2',
|
||||
parentNodeId: '2',
|
||||
realNodeId: '2',
|
||||
promptId: 'test-prompt-id'
|
||||
}
|
||||
})
|
||||
api.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for preview to be handled
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => window['__previewHandled'] === true,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that preview was set for the correct node
|
||||
const result = await comfyPage.page.evaluate(() => {
|
||||
return {
|
||||
previewImages: window['app'].nodePreviewImages,
|
||||
lastUrl: window['__lastPreviewUrl']
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.previewImages['2']).toBeDefined()
|
||||
expect(result.previewImages['2']).toHaveLength(1)
|
||||
expect(result.previewImages['2'][0]).toBe(result.lastUrl)
|
||||
})
|
||||
|
||||
test('Clears old previews when new preview arrives', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Set up initial preview
|
||||
const initialBlobUrl = await comfyPage.page.evaluate(() => {
|
||||
const blob = new Blob(['initial image'], { type: 'image/png' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window['app'].nodePreviewImages['3'] = [url]
|
||||
return url
|
||||
})
|
||||
|
||||
// Create spy to track URL revocations
|
||||
await previewHelper.setupPreviewTracking()
|
||||
|
||||
// Mock the handler to revoke old previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
const { displayNodeId } = event.detail
|
||||
window['app'].revokePreviews(displayNodeId)
|
||||
const newBlob = new Blob(['new image'], { type: 'image/png' })
|
||||
const newUrl = URL.createObjectURL(newBlob)
|
||||
window['app'].nodePreviewImages[displayNodeId] = [newUrl]
|
||||
})
|
||||
})
|
||||
|
||||
// Trigger new preview for same node
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const event = new CustomEvent('b_preview_with_metadata', {
|
||||
detail: {
|
||||
blob: new Blob(['new image'], { type: 'image/png' }),
|
||||
nodeId: '3',
|
||||
displayNodeId: '3',
|
||||
parentNodeId: '3',
|
||||
realNodeId: '3',
|
||||
promptId: 'test-prompt-id'
|
||||
}
|
||||
})
|
||||
api.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check that old URL was revoked
|
||||
const finalRevokedUrls = await previewHelper.getRevokedUrls()
|
||||
expect(finalRevokedUrls).toContain(initialBlobUrl)
|
||||
|
||||
// Check that new preview replaced old one
|
||||
const newPreviewImages = await previewHelper.getNodePreviews('3')
|
||||
|
||||
expect(newPreviewImages).toHaveLength(1)
|
||||
expect(newPreviewImages[0]).not.toBe(initialBlobUrl)
|
||||
})
|
||||
|
||||
test('Associates preview with correct display node in subgraph', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Mock handler that stores metadata
|
||||
await previewHelper.setupPreviewTracking()
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['__previewMetadata'] = {}
|
||||
const api = window['app'].api
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
const { displayNodeId, nodeId, parentNodeId, realNodeId, promptId } =
|
||||
event.detail
|
||||
window['__previewMetadata'][displayNodeId] = {
|
||||
nodeId,
|
||||
displayNodeId,
|
||||
parentNodeId,
|
||||
realNodeId,
|
||||
promptId
|
||||
}
|
||||
// Still create the preview
|
||||
const url = URL.createObjectURL(event.detail.blob)
|
||||
window['app'].nodePreviewImages[displayNodeId] = [url]
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate preview from a subgraph node
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const event = new CustomEvent('b_preview_with_metadata', {
|
||||
detail: {
|
||||
blob: new Blob(['subgraph preview'], { type: 'image/png' }),
|
||||
nodeId: '10:5:3', // Nested execution ID
|
||||
displayNodeId: '10', // Top-level display node
|
||||
parentNodeId: '10:5', // Parent subgraph
|
||||
realNodeId: '3', // Actual node ID within subgraph
|
||||
promptId: 'test-prompt-id'
|
||||
}
|
||||
})
|
||||
api.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check that preview is associated with display node
|
||||
const metadata = await comfyPage.page.evaluate(
|
||||
() => window['__previewMetadata']
|
||||
)
|
||||
expect(metadata['10']).toBeDefined()
|
||||
expect(metadata['10'].nodeId).toBe('10:5:3')
|
||||
expect(metadata['10'].displayNodeId).toBe('10')
|
||||
expect(metadata['10'].parentNodeId).toBe('10:5')
|
||||
expect(metadata['10'].realNodeId).toBe('3')
|
||||
|
||||
// Check that preview exists for display node
|
||||
const previews = await comfyPage.page.evaluate(
|
||||
() => window['app'].nodePreviewImages
|
||||
)
|
||||
expect(previews['10']).toBeDefined()
|
||||
expect(previews['10']).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('Maintains backward compatibility with b_preview event', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Track both events
|
||||
const eventsFired = await comfyPage.page.evaluate(() => {
|
||||
const events: string[] = []
|
||||
const api = window['app'].api
|
||||
|
||||
api.addEventListener('b_preview', () => {
|
||||
events.push('b_preview')
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview_with_metadata', () => {
|
||||
events.push('b_preview_with_metadata')
|
||||
})
|
||||
|
||||
window['__eventsFired'] = events
|
||||
return events
|
||||
})
|
||||
|
||||
// Trigger b_preview_with_metadata
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const blob = new Blob(['test image'], { type: 'image/png' })
|
||||
|
||||
// Simulate the API behavior
|
||||
api.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob,
|
||||
nodeId: '2',
|
||||
displayNodeId: '2',
|
||||
parentNodeId: '2',
|
||||
realNodeId: '2',
|
||||
promptId: 'test-prompt-id'
|
||||
})
|
||||
|
||||
// Also dispatch legacy event as the API would
|
||||
api.dispatchCustomEvent('b_preview', blob)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check that both events were fired
|
||||
const finalEvents = await comfyPage.page.evaluate(
|
||||
() => window['__eventsFired']
|
||||
)
|
||||
expect(finalEvents).toContain('b_preview_with_metadata')
|
||||
expect(finalEvents).toContain('b_preview')
|
||||
})
|
||||
})
|
||||
237
browser_tests/tests/subgraphExecutionProgress.spec.ts
Normal file
237
browser_tests/tests/subgraphExecutionProgress.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
SubgraphTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Subgraph Execution Progress', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
test.setTimeout(30000) // Increase timeout for subgraph tests
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let subgraphHelper: SubgraphTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
subgraphHelper = new SubgraphTestHelper(comfyPage.page)
|
||||
// Share the same test ID to access the same window properties
|
||||
subgraphHelper.setTestId(executionHelper.getTestId())
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Shows progress for nodes inside subgraphs', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/nested-subgraph-test')
|
||||
|
||||
// Get reference to the subgraph node
|
||||
const subgraphNode = await comfyPage.getNodeRefById(10)
|
||||
expect(subgraphNode).toBeDefined()
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command to avoid click issues
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real progress events from subgraph execution
|
||||
await subgraphHelper.waitForNestedNodeProgress(1)
|
||||
|
||||
// Wait for progress to be applied to the subgraph node
|
||||
await executionHelper.waitForGraphNodeProgress([10])
|
||||
|
||||
// Check that the subgraph node shows aggregated progress
|
||||
const subgraphProgress = await subgraphNode.getProperty('progress')
|
||||
|
||||
// The progress should be aggregated from child nodes
|
||||
expect(subgraphProgress).toBeDefined()
|
||||
expect(subgraphProgress).toBeGreaterThan(0)
|
||||
expect(subgraphProgress).toBeLessThanOrEqual(1)
|
||||
|
||||
// Wait for stroke style to be applied
|
||||
await comfyPage.page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node?.strokeStyles?.['running']) return false
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color === '#0f0'
|
||||
},
|
||||
10,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check stroke style
|
||||
const strokeStyle = await comfyPage.page.evaluate((nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node || !node.strokeStyles || !node.strokeStyles['running']) {
|
||||
return null
|
||||
}
|
||||
return node.strokeStyles['running'].call(node)
|
||||
}, 10)
|
||||
|
||||
expect(strokeStyle).toEqual({ color: '#0f0' })
|
||||
})
|
||||
|
||||
test('Handles deeply nested subgraph execution', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/nested-subgraph-test')
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command to avoid click issues
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real progress events from deeply nested subgraph execution
|
||||
await subgraphHelper.waitForNestedNodeProgress(2)
|
||||
|
||||
// Wait for progress to be applied to the top-level subgraph node
|
||||
await executionHelper.waitForGraphNodeProgress([10])
|
||||
|
||||
// Check that top-level subgraph shows progress
|
||||
const subgraphNode = await comfyPage.getNodeRefById(10)
|
||||
const progress = await subgraphNode.getProperty('progress')
|
||||
|
||||
expect(progress).toBeDefined()
|
||||
expect(progress).toBeGreaterThan(0)
|
||||
expect(progress).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('Shows running state for parent nodes when child executes', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/nested-subgraph-test')
|
||||
|
||||
// Track which nodes have running stroke style
|
||||
const getRunningNodes = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
const runningNodes: number[] = []
|
||||
const nodes = window['app'].graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
node.strokeStyles?.['running'] &&
|
||||
node.strokeStyles['running'].call(node)?.color === '#0f0'
|
||||
) {
|
||||
runningNodes.push(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return runningNodes
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await comfyPage.page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const nodes = window['app'].graph.nodes
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
node.strokeStyles?.['running'] &&
|
||||
node.strokeStyles['running'].call(node)?.color === '#0f0'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
.catch(() => {
|
||||
// If timeout, continue anyway
|
||||
})
|
||||
|
||||
// Initially no nodes should be running
|
||||
let runningNodes = await getRunningNodes()
|
||||
expect(runningNodes).toHaveLength(0)
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command to avoid click issues
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real nested node execution progress
|
||||
await subgraphHelper.waitForNestedNodeProgress(1)
|
||||
|
||||
// Wait for parent subgraph to show as running
|
||||
await comfyPage.page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node?.strokeStyles?.['running']) return false
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color === '#0f0'
|
||||
},
|
||||
10,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Parent subgraph should show as running
|
||||
runningNodes = await getRunningNodes()
|
||||
expect(runningNodes).toContain(10)
|
||||
|
||||
// Wait for the execution to complete naturally
|
||||
const testId = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
// Check if execution is finished (no more running nodes)
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running'
|
||||
).length
|
||||
|
||||
return runningNodes === 0
|
||||
},
|
||||
testId,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Add a small delay to ensure UI updates
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Wait for parent subgraph to no longer be running
|
||||
await comfyPage.page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node?.strokeStyles?.['running']) return true
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color !== '#0f0'
|
||||
},
|
||||
10,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Parent should no longer be running
|
||||
runningNodes = await getRunningNodes()
|
||||
expect(runningNodes).not.toContain(10)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user