Files
DumiFlex-ComfyUI-Wildcard-P…/tests/test_nodes_assembler.py
T
DumiFlex bb40882238 feat(canvas): assembler template gets the rich editor, keeping its link input
The template input now renders through RichTextInput, so it has the booru tag
autocomplete, `$var` chips with producer attribution, and the same editing
behaviour as every other WP text surface.

No payload transition. It is the SAME `template` widget: same name, same
position in `widgets_values`, same plain string value — only the renderer
changed. A workflow saved before this loads its template back untouched;
verified by loading a hand-written pre-change workflow JSON.

The seam is `widgetType`. The frontend picks a widget constructor with
`t.widgetType ?? t.type` while the socket keeps `t.type`, so tagging the input
spec routes the lookup to us without touching what the socket accepts. It has
to go through `extra_dict`: V3's `widget_type=` parameter would ALSO change
the socket, because `WidgetInput.get_io_type` returns widget_type when set —
the input would stop being STRING and no upstream STRING output could feed it.
`extra_dict` is merged last in `Input.as_dict` and never consulted by
`get_io_type`. The new schema test asserts both halves together, since either
one alone passes while the feature is broken.

Keeping the link input needs one more thing: ComfyUI only calls
`addInput(name, spec.type, {widget})` when the widget the factory returns has
no `socketless` flag, and `createDomWidgetHost` hardcoded `socketless: true`.
Hence the new `socketed` option — correct for every other WP widget, exactly
wrong for this one. While a link IS connected the editor goes read-only with
a placeholder saying so, matching the stock multiline widget. Derived from the
link rather than the widget's `computedDisabled`, which is recomputed lazily
during a draw and still read `true` a second after the link was removed.

Why a separate file and not `assembler.ts`: the helper widget must stay
socketless (it is an editor, not a value), and the two widgets need opposite
answers to that one question.

The helper's template mutators lose their `inputEl` branches. Those had been
dead for a while — the frontend moved widget values into a store, so the
textarea at `widget.inputEl` is detached and unrendered, and each of those
functions was really working through the `w.value` write sitting next to it.
Insert is the exception: it needs a caret, so it asks the editor through a
WeakMap hook instead of replacing the whole string.

Verified live on frontend 1.48.6 with Vue Nodes on: legacy value restores,
STRING link connects and disconnects, serialize round-trips, clear / remove /
insert all propagate, `$` autocomplete lists upstream vars with attribution,
tag autocomplete and its legend work, and a full Context → assembler chain
previews correctly.
2026-08-07 02:20:18 +03:00

82 lines
3.4 KiB
Python

"""Unit tests for nodes.assembler_node.WPPromptAssembler."""
from wp_nodes.assembler_node import WPPromptAssembler
from wp_nodes.types import ContextPayload
class TestWPPromptAssemblerSchema:
def test_schema_basic(self):
schema = WPPromptAssembler.define_schema()
assert schema.node_id == "WP_PromptAssembler"
assert schema.display_name == "WP Prompt Assembler"
assert schema.category == "wildcard-pipeline"
def test_inputs(self):
schema = WPPromptAssembler.define_schema()
names = [s.name for s in schema.inputs]
assert names == ["context", "template"]
ctx_in, tmpl_in = schema.inputs
assert ctx_in.type_name == "PIPELINE_CONTEXT"
assert tmpl_in.type_name == "STRING"
assert tmpl_in.multiline is True
def test_template_uses_our_editor_without_losing_the_string_socket(self):
"""The whole point of routing `widgetType` through `extra_dict`.
The frontend picks a widget constructor with `widgetType ?? type` and
adds the socket from `type`, so these two facts have to hold at the
same time: the widget is ours, the socket is still STRING and can be
driven by an upstream STRING output.
Using V3's `widget_type=` parameter instead would satisfy the first
and break the second — `WidgetInput.get_io_type` returns widget_type
when set, so the socket would become `WP_TEMPLATE_EDITOR` and nothing
could connect to it. Assert both together; either one alone passes
while the feature is broken.
"""
schema = WPPromptAssembler.define_schema()
tmpl_in = next(s for s in schema.inputs if s.name == "template")
assert tmpl_in.type_name == "STRING"
assert (tmpl_in.extra_dict or {}).get("widgetType") == "WP_TEMPLATE_EDITOR"
def test_outputs(self):
schema = WPPromptAssembler.define_schema()
assert len(schema.outputs) == 1
assert schema.outputs[0].type_name == "STRING"
class TestWPPromptAssemblerExecute:
def test_resolves_single_var(self):
payload = ContextPayload(context={"style": "photoreal"}, debug={})
out = WPPromptAssembler.execute(context=payload, template="A $style shot")
assert out.values == ("A photoreal shot",)
def test_resolves_multiple_vars(self):
payload = ContextPayload(
context={"style": "photo", "subject": "knight", "light": "soft"},
debug={},
)
out = WPPromptAssembler.execute(
context=payload,
template="A $style $subject in $light light",
)
assert out.values == ("A photo knight in soft light",)
def test_missing_var_dropped_with_whitespace_cleanup(self):
# Missing vars resolve to empty string and surrounding whitespace is
# collapsed so the prompt stays clean.
payload = ContextPayload(context={}, debug={})
out = WPPromptAssembler.execute(context=payload, template="$unknown here")
assert out.values == ("here",)
def test_dollar_escape(self):
payload = ContextPayload(context={"x": "1"}, debug={})
out = WPPromptAssembler.execute(context=payload, template="$$ costs $x")
assert out.values == ("$ costs 1",)
def test_empty_template(self):
payload = ContextPayload(context={"x": "1"}, debug={})
out = WPPromptAssembler.execute(context=payload, template="")
assert out.values == ("",)