Files
ltdrdata-ComfyUI-Impact-Pack/tests/test_e2e_wildcard_cache.py
T
Dr.Lt.Data d246db9e27 Support constant wildcard inputs and preserve list seeds
Resolve direct string constants before execution and retain requested seeds
for list-producing inputs. Preserve native widget values and scalar reuse.

Cover cold queues, mapped lists and Control combinations with actual core
cache checks and Chrome tests. Limit the Bypass xfail to the known Processor
case so Encode failures remain visible.
2026-11-03 20:46:14 +09:00

482 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Real queue/cache behavior and native wildcard widget restoration in Chrome.
Use an isolated ComfyUI server. Encode cases use an installed checkpoint,
defaulting to v1-5-pruned-emaonly-fp16.safetensors (no downloads).
"""
from argparse import Namespace
import json
import os
from pathlib import Path
from uuid import uuid4
import pytest
from playwright.sync_api import expect
from e2e_ui import browser_session
from test_e2e_widget_catalog import export_workflow, full_zoom, rendered, setting, show_node, widget_state
NODE_TYPES = ["ImpactWildcardProcessor", "ImpactWildcardEncode"]
ROOT = Path(__file__).resolve().parents[1]
@pytest.fixture
def ui(tmp_path, request):
args = Namespace(base_url=os.environ.get("IMPACT_E2E_BASE_URL", "http://127.0.0.1:18188"),
browser_channel="chrome", headed=False, timeout=120,
artifacts=Path(os.environ.get("IMPACT_E2E_ARTIFACTS", str(tmp_path))) / request.node.name)
with browser_session(args) as browser:
browser.artifacts = args.artifacts
browser.page.keyboard.press("Escape")
browser.new_workflow()
full_zoom(browser)
yield browser
def graph(ui, node_type):
node_id = ui.add_node(node_type, node_type, 550, 170)
preview_id = ui.add_node("Preview as Text", "PreviewAny", 1150, 450)
ui.connect(node_type, "PreviewAny", source_slot=3 if node_type == "ImpactWildcardEncode" else 0)
if node_type == "ImpactWildcardEncode":
ui.add_node("Load Checkpoint", "CheckpointLoaderSimple", 100, 500)
checkpoint = os.environ.get("IMPACT_E2E_CHECKPOINT", "v1-5-pruned-emaonly-fp16.safetensors")
ui.choose("CheckpointLoaderSimple", "ckpt_name", checkpoint)
ui.connect("CheckpointLoaderSimple", node_type)
ui.connect("CheckpointLoaderSimple", node_type, 1, source_slot=1)
show_node(ui, node_id)
ui.choose(node_type, "control_after_generate", "fixed")
return node_id, preview_id
def cached(entry):
events = [data["nodes"] for event, data in entry["status"]["messages"] if event == "execution_cached"]
assert len(events) == 1, entry["status"]
return set(map(str, events[0]))
def run(ui, preview_id, expected, label):
entry = ui.run()
(ui.artifacts / f"{label}.json").write_text(json.dumps(entry, indent=2) + "\n")
assert entry["outputs"][preview_id]["text"][0] == expected
expect(ui.page.get_by_role("textbox", name="preview_text", exact=True)).to_have_value(expected)
return entry
def set_text(ui, node_type, widget, value):
ui.page.mouse.dblclick(*ui.point(node_type, widget=widget), delay=100)
editor = ui.page.locator("input.value:visible")
editor.fill(value)
editor.press("Enter")
def native_roundtrip(ui, node_id, *other_ids):
"""Compare rendered widget names/order/values and the native serialization."""
trusted = json.loads((ROOT / "tests/workflows/all-impact-nodes.widgets.json").read_text())
states = {}
for target_id in (node_id, *other_ids):
show_node(ui, target_id)
state = widget_state(ui, target_id)
reference = next(node for node in trusted["nodes"] if node["type"] == state["type"])
assert [w["name"] for w in state["widgets"]] == [w["name"] for w in reference["widgets"]]
states[target_id] = state
workflow = ui.artifacts / "saved.json"
export_workflow(ui, workflow)
saved = json.loads(workflow.read_text())
ui.new_workflow()
ui.load_workflow(workflow, len(saved["nodes"]))
full_zoom(ui)
for target_id, before in states.items():
show_node(ui, target_id)
assert json.dumps(widget_state(ui, target_id), sort_keys=True) == json.dumps(before, sort_keys=True)
roundtrip = ui.artifacts / "roundtrip.json"
export_workflow(ui, roundtrip)
restored = json.loads(roundtrip.read_text())
for field, default in (("widgets_values", []), ("widgets_values_named", {})):
assert json.dumps({str(n["id"]): n.get(field, default) for n in restored["nodes"]}, sort_keys=True) == json.dumps({
str(n["id"]): n.get(field, default) for n in saved["nodes"]}, sort_keys=True)
# Native loading can reorder link records; their IDs and endpoints must
# remain identical. Widget arrays above deliberately retain their order.
assert sorted(restored["links"]) == sorted(saved["links"])
assert restored.get("definitions") == saved.get("definitions")
(ui.artifacts / "widget-state.json").write_text(json.dumps(states, indent=2) + "\n")
show_node(ui, node_id)
return states[node_id]
def input_slot(ui, node_type, name):
return ui.app.evaluate("""(app, {type, name}) =>
app.graph._nodes.find(n => n.type === type).inputs.findIndex(i => i.name === name)
""", {"type": node_type, "name": name})
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_linked_inputs_and_subgraph_native_queue(ui, node_type, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
source_id = ui.add_node("Concatenate Text", "StringConcatenate", 100, 650)
show_node(ui, source_id)
# Neither fragment contains a complete choice, so scanning upstream input
# strings cannot determine the seed dependence of their combined output.
ui.page.get_by_placeholder("string_a", exact=True).fill("{red|")
ui.page.get_by_placeholder("string_b", exact=True).fill("green|blue}")
show_node(ui, node_id)
ui.connect("StringConcatenate", node_type, target_slot=input_slot(ui, node_type, "populated_text"))
for seed, expected in ((2, "red"), (5, "blue")):
ui.set_number(node_type, "seed", seed)
entry = run(ui, preview_id, expected, f"linked-text-{seed}")
assert entry["prompt"][2][node_id]["inputs"]["populated_text"] == [source_id, 0]
assert entry["prompt"][2][node_id]["inputs"]["seed"] == seed
seed_id = ui.add_node("ImpactInt", "ImpactInt", 850, 180)
show_node(ui, seed_id)
ui.set_number("ImpactInt", "value", 2)
show_node(ui, node_id)
ui.connect("ImpactInt", node_type, target_slot=input_slot(ui, node_type, "seed"))
entry = run(ui, preview_id, "red", "linked-seed")
assert entry["prompt"][2][node_id]["inputs"]["seed"] == [seed_id, 0]
# Convert the real upstream node using the frontend menu. No graph mutation
# or hand-written workflow JSON is used to construct the subgraph.
show_node(ui, source_id)
title = ui.page.evaluate("""({app, id}) => {
const n = app.graph.getNodeById(id);
return app.canvasPosToClientPos([n.pos[0] + 40, n.pos[1] - 15]);
}""", {"app": ui.app, "id": source_id})
ui.page.mouse.click(*title)
ui.page.mouse.click(*title, button="right")
ui.page.get_by_role("menuitem", name="Convert to Subgraph", exact=True).click()
ui.page.wait_for_function("""({app, id}) => !app.graph.getNodeById(id)
""", arg={"app": ui.app, "id": source_id})
native_roundtrip(ui, node_id)
saved = json.loads((ui.artifacts / "saved.json").read_text())
assert any(n["type"] == "StringConcatenate"
for subgraph in saved["definitions"]["subgraphs"] for n in subgraph["nodes"])
for seed, expected in ((5, "blue"), (2, "red")):
show_node(ui, seed_id)
ui.set_number("ImpactInt", "value", seed)
entry = run(ui, preview_id, expected, f"restored-subgraph-{seed}")
inputs = entry["prompt"][2][node_id]["inputs"]
assert inputs["seed"] == [seed_id, 0]
assert isinstance(inputs["populated_text"], list)
upstream_id = inputs["populated_text"][0]
assert entry["prompt"][2][upstream_id]["class_type"] == "StringConcatenate"
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_populate_reuses_cache_only_for_same_expansion(ui, node_type):
node_id, preview_id = graph(ui, node_type)
prefix = "populate cache "
# The source participates in the cache key. A unique comment isolates this
# case even on a warm LRU server, while keeping expected output deterministic.
source = f"# cache case {uuid4().hex}\n" + prefix + "{red|green|blue}"
ui.page.get_by_placeholder("Wildcard Prompt (User input)", exact=True).fill(source)
# Trusted pre-change selections: 0 and 1 -> green; 2 -> red.
for seed, color in ((0, "green"), (1, "green"), (2, "red")):
ui.set_number(node_type, "seed", seed)
entry = run(ui, preview_id, prefix + color, f"populate-cache-{seed}")
expect(ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True)).to_have_value(
prefix + color)
if seed == 1:
assert {node_id, preview_id} <= cached(entry)
else:
assert {node_id, preview_id}.isdisjoint(cached(entry))
assert entry["prompt"][2][node_id]["inputs"]["seed"] == 0
assert next(w["value"] for w in widget_state(ui, node_id)["widgets"] if w["name"] == "seed") == seed
saved_node = next(n for n in entry["prompt"][3]["extra_pnginfo"]["workflow"]["nodes"]
if str(n["id"]) == node_id)
assert saved_node["widgets_values"][:3] == [source, prefix + color, "reproduce"]
seed_index = 3 if node_type == "ImpactWildcardProcessor" else 5
assert saved_node["widgets_values"][seed_index] == seed
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
@pytest.mark.parametrize("syntax_first", [True, False], ids=["syntax-first", "syntax-last"])
def test_mapped_list_preserves_each_seed(ui, node_type, syntax_first, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
text_id = ui.add_node("Text", "PrimitiveString", 100, 650)
set_text(ui, "PrimitiveString", "value", "{red|green|blue}")
plain = f"plain {uuid4().hex}"
other_id = ui.add_node("Text (Multiline)", "PrimitiveStringMultiline", 100, 850)
ui.page.get_by_placeholder("value", exact=True).fill(plain)
list_id = ui.add_node("Make List (Any)", "ImpactMakeAnyList", 700, 800)
sources = ("PrimitiveString", "PrimitiveStringMultiline") if syntax_first else (
"PrimitiveStringMultiline", "PrimitiveString")
for index, source in enumerate(sources, 1):
ui.connect(source, "ImpactMakeAnyList", target_slot=input_slot(ui, "ImpactMakeAnyList", f"value{index}"))
ui.connect("ImpactMakeAnyList", node_type, target_slot=input_slot(ui, node_type, "populated_text"))
native_roundtrip(ui, node_id, list_id)
for seed, color in ((2, "red"), (5, "blue")):
show_node(ui, node_id)
ui.set_number(node_type, "seed", seed)
entry = ui.run()
(ui.artifacts / f"list-{seed}.json").write_text(json.dumps(entry, indent=2) + "\n")
expected = [color, plain] if syntax_first else [plain, color]
assert entry["outputs"][preview_id]["text"] == expected
show_node(ui, preview_id)
expect(ui.page.get_by_role("textbox", name="preview_text", exact=True)).to_have_value("\n\n".join(expected))
assert entry["prompt"][2][node_id]["inputs"]["seed"] == seed
assert {node_id, preview_id}.isdisjoint(cached(entry))
if seed == 5:
assert {text_id, other_id, list_id} <= cached(entry)
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_scalar_runtime_text_still_reuses_learned_seed(ui, node_type):
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
source_id = ui.add_node("Concatenate Text", "StringConcatenate", 100, 650)
text = f"learned {uuid4().hex}"
ui.page.get_by_placeholder("string_a", exact=True).fill(text)
ui.page.get_by_placeholder("string_b", exact=True).fill(" text")
ui.connect("StringConcatenate", node_type, target_slot=input_slot(ui, node_type, "populated_text"))
for seed in (731, 732):
show_node(ui, node_id)
ui.set_number(node_type, "seed", seed)
entry = run(ui, preview_id, text + " text", f"learned-{seed}")
assert entry["prompt"][2][node_id]["inputs"]["seed"] == 731
assert (node_id in cached(entry)) is (seed == 732)
show_node(ui, source_id)
ui.page.get_by_placeholder("string_a", exact=True).fill(f"{text} {{red|")
ui.page.get_by_placeholder("string_b", exact=True).fill("green|blue}")
for seed, expected in ((2, "red"), (5, "blue")):
show_node(ui, node_id)
ui.set_number(node_type, "seed", seed)
entry = run(ui, preview_id, f"{text} {expected}", f"learned-to-syntax-{seed}")
assert entry["prompt"][2][node_id]["inputs"]["seed"] == seed
assert {node_id, preview_id}.isdisjoint(cached(entry))
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_reproduce_native_workflow_then_populate(ui, node_type, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
ui.page.get_by_placeholder("Wildcard Prompt (User input)", exact=True).fill("{red|green|blue}")
# Set a known saved expansion through the UI so a population bug cannot
# prevent this test from reaching the reproduce/load behavior it protects.
ui.choose(node_type, "mode", "fixed")
ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True).fill("red")
ui.choose(node_type, "mode", "reproduce")
ui.set_number(node_type, "seed", 5)
before = native_roundtrip(ui, node_id)
values = {w["name"]: w["value"] for w in before["widgets"]}
assert (values["wildcard_text"], values["populated_text"], values["mode"], values["seed"]) == (
"{red|green|blue}", "red", "reproduce", 5)
run(ui, preview_id, "red", "restored-reproduce")
ui.page.wait_for_function("""({app, id}) =>
app.graph.getNodeById(id).widgets.find(w => w.name === 'mode').value === 'populate'
""", arg={"app": ui.app, "id": node_id})
run(ui, preview_id, "blue", "next-populate")
assert widget_state(ui, node_id)["widgets"] == [
dict(w, value="populate") if w["name"] == "mode" else
dict(w, value="blue") if w["name"] == "populated_text" else w for w in before["widgets"]
]
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_plain_text_reuses_downstream_cache(ui, node_type):
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
ui.choose(node_type, "control_after_generate", "increment")
ui.set_number(node_type, "seed", 731)
populated = ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True)
text = f"ordinary prompt {uuid4().hex}"
populated.fill(text)
first = run(ui, preview_id, text, "first")
second = run(ui, preview_id, text, "second")
assert {node_id, preview_id}.isdisjoint(cached(first))
assert {node_id, preview_id} <= cached(second)
assert second["prompt"][2][node_id]["inputs"]["seed"] == 0
ui.page.wait_for_function("""({app, id}) =>
app.graph.getNodeById(id).widgets.find(w => w.name === 'seed').value === 733
""", arg={"app": ui.app, "id": node_id})
populated.fill(text + " changed")
changed = run(ui, preview_id, text + " changed", "changed")
assert {node_id, preview_id}.isdisjoint(cached(changed))
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
@pytest.mark.parametrize("source_type", ["PrimitiveString", "PrimitiveStringMultiline"])
@pytest.mark.parametrize("field", ["populated_text", "wildcard_text"])
def test_constant_text_link_cache_and_native_restore(ui, node_type, source_type, field, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "control_after_generate", "increment")
ui.set_number(node_type, "seed", 731)
title = "Text" if source_type == "PrimitiveString" else "Text (Multiline)"
source_id = ui.add_node(title, source_type, 100, 650)
def set_source_text(value):
show_node(ui, source_id)
if source_type == "PrimitiveStringMultiline":
ui.page.get_by_placeholder("value", exact=True).fill(value)
else:
set_text(ui, source_type, "value", value)
text = f"constant prompt {uuid4().hex}"
set_source_text(text)
show_node(ui, node_id)
ui.connect(source_type, node_type, target_slot=input_slot(ui, node_type, field))
native_roundtrip(ui, node_id)
for index in range(2):
entry = run(ui, preview_id, text, f"constant-{index}")
inputs = entry["prompt"][2][node_id]["inputs"]
assert inputs[field] == [source_id, 0]
assert inputs["seed"] == 0
if index:
assert {node_id, preview_id} <= cached(entry)
else:
assert {node_id, preview_id}.isdisjoint(cached(entry))
assert next(w["value"] for w in widget_state(ui, node_id)["widgets"] if w["name"] == "seed") == 733
set_source_text(text + " changed")
changed = run(ui, preview_id, text + " changed", "constant-changed")
assert {node_id, preview_id}.isdisjoint(cached(changed))
# LRU can retain an identical syntax graph from another parameterized case.
syntax = f"{text} {{red|green|blue}}"
set_source_text(syntax)
show_node(ui, node_id)
ui.choose(node_type, "control_after_generate", "fixed")
for seed, expected in ((2, "red"), (5, "blue")):
ui.set_number(node_type, "seed", seed)
entry = run(ui, preview_id, f"{text} {expected}", f"constant-syntax-{seed}")
assert entry["prompt"][2][node_id]["inputs"]["seed"] == (seed if field == "populated_text" else 0)
seed_id = ui.add_node("ImpactInt", "ImpactInt", 850, 650)
ui.set_number("ImpactInt", "value", 2)
ui.connect("ImpactInt", node_type, target_slot=input_slot(ui, node_type, "seed"))
before = native_roundtrip(ui, node_id)
for source_text, label in ((syntax, "syntax"), (text + " linked seed", "plain")):
set_source_text(source_text)
for seed, expected in ((2, "red"), (5, "blue")):
show_node(ui, seed_id)
ui.set_number("ImpactInt", "value", seed)
show_node(ui, node_id)
entry = run(ui, preview_id, f"{text} {expected}" if label == "syntax" else source_text, f"linked-seed-{label}-{seed}")
inputs = entry["prompt"][2][node_id]["inputs"]
assert inputs["seed"] == [seed_id, 0]
assert inputs[field] == [source_id, 0]
assert {node_id, preview_id}.isdisjoint(cached(entry))
show_node(ui, node_id)
assert next(w for w in widget_state(ui, node_id)["widgets"] if w["name"] == "seed") == next(
w for w in before["widgets"] if w["name"] == "seed")
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_runtime_population_source_preserves_saved_text(ui, node_type, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
previous = f"saved text {uuid4().hex}"
ui.choose(node_type, "mode", "fixed")
ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True).fill(previous)
ui.choose(node_type, "mode", "populate")
ui.set_number(node_type, "seed", 2)
source_id = ui.add_node("Concatenate Text", "StringConcatenate", 100, 650)
show_node(ui, source_id)
ui.page.get_by_placeholder("string_a", exact=True).fill("{red|")
ui.page.get_by_placeholder("string_b", exact=True).fill("green|blue}")
show_node(ui, node_id)
ui.connect("StringConcatenate", node_type, target_slot=input_slot(ui, node_type, "wildcard_text"))
before = native_roundtrip(ui, node_id)
entry = run(ui, preview_id, previous, "unsupported-population")
inputs = entry["prompt"][2][node_id]["inputs"]
assert (inputs["wildcard_text"], inputs["populated_text"], inputs["mode"], inputs["seed"]) == (
[source_id, 0], previous, "populate", 2)
assert widget_state(ui, node_id) == before
native_roundtrip(ui, node_id)
constant_id = ui.add_node("Text", "PrimitiveString", 850, 750)
show_node(ui, constant_id)
set_text(ui, "PrimitiveString", "value", "{red|green|blue}")
show_node(ui, node_id)
ui.connect("PrimitiveString", node_type, target_slot=input_slot(ui, node_type, "wildcard_text"))
entry = run(ui, preview_id, "red", "supported-population")
inputs = entry["prompt"][2][node_id]["inputs"]
assert (inputs["wildcard_text"], inputs["populated_text"], inputs["mode"], inputs["seed"]) == (
[constant_id, 0], "red", "reproduce", 0)
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
@pytest.mark.parametrize("source_type", [None, "PrimitiveString", "PrimitiveStringMultiline"],
ids=["literal", "string", "multiline"])
def test_execution_metadata_restores_populated_text(ui, node_type, source_type, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True).fill("previous text")
ui.choose(node_type, "mode", "populate")
ui.set_number(node_type, "seed", 2)
if source_type:
title = "Text" if source_type == "PrimitiveString" else "Text (Multiline)"
source_id = ui.add_node(title, source_type, 100, 650)
ui.connect(source_type, node_type, target_slot=input_slot(ui, node_type, "wildcard_text"))
def set_source(value):
show_node(ui, source_id if source_type else node_id)
if source_type == "PrimitiveString":
set_text(ui, source_type, "value", value)
else:
placeholder = "value" if source_type else "Wildcard Prompt (User input)"
ui.page.get_by_placeholder(placeholder, exact=True).fill(value)
set_source("{red|green|blue}")
show_node(ui, node_id)
before = native_roundtrip(ui, node_id)
entry = run(ui, preview_id, "red", "generated")
# Load the native workflow captured by the server, before UI feedback can
# repair stale metadata through another Export.
workflow = entry["prompt"][3]["extra_pnginfo"]["workflow"]
path = ui.artifacts / "execution-workflow.json"
path.write_text(json.dumps(workflow, indent=2) + "\n")
ui.new_workflow()
ui.load_workflow(path, len(workflow["nodes"]))
full_zoom(ui)
show_node(ui, node_id)
expected = [dict(w, value="red") if w["name"] == "populated_text" else
dict(w, value="reproduce") if w["name"] == "mode" else w for w in before["widgets"]]
assert widget_state(ui, node_id)["widgets"] == expected
saved_node = next(n for n in workflow["nodes"] if str(n["id"]) == node_id)
assert saved_node["widgets_values"] == [w["value"] for w in expected]
assert saved_node["widgets_values_named"] == {w["name"]: w["value"] for w in expected}
set_source("changed after loading")
show_node(ui, node_id)
run(ui, preview_id, "red", "reproduced")
run(ui, preview_id, "changed after loading", "next-populate")
@pytest.mark.parametrize("node_type", NODE_TYPES)
@pytest.mark.parametrize("text", ["{red|\ngreen|blue}", "{red|{green}|blue}"], ids=["multiline", "nested"])
def test_fixed_syntax_preserves_seed(ui, node_type, text):
_, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True).fill(text)
for seed, expected in ((2, "red"), (5, "blue")):
ui.set_number(node_type, "seed", seed)
run(ui, preview_id, expected, f"fixed-{seed}")
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
@pytest.mark.parametrize("node_type", NODE_TYPES)
def test_fixed_native_workflow_preserves_widgets(ui, node_type, restore_by_name):
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
node_id, preview_id = graph(ui, node_type)
ui.choose(node_type, "mode", "fixed")
ui.page.get_by_placeholder("Wildcard Prompt (User input)", exact=True).fill("unused {red|blue}")
ui.page.get_by_placeholder("Populated Prompt (Will be generated automatically)", exact=True).fill("saved plain α")
ui.set_number(node_type, "seed", 731)
ui.choose(node_type, "control_after_generate", "increment")
rendered(ui)
before = native_roundtrip(ui, node_id)
run(ui, preview_id, "saved plain α", "restored")
assert widget_state(ui, node_id)["widgets"] == [
dict(widget, value=732) if widget["name"] == "seed" else widget for widget in before["widgets"]
]