Clamp finite mask values to [0, 1] before uint8 contour conversion while preserving input arithmetic dtype, batch handling and combined behavior. Add CPU and native-workflow regression coverage for range boundaries, tutorial mask behavior, widget serialization and fresh browser execution. Document the isolated test environment and regression verification scope.
65 lines
3.6 KiB
Python
65 lines
3.6 KiB
Python
"""Native PNG/JSON compatibility using the original extension tutorials.
|
|
|
|
Set IMPACT_TUTORIALS_DIR to a ComfyUI-extension-tutorials checkout.
|
|
This checks loading and saved widgets; it does not run model inference.
|
|
"""
|
|
from argparse import Namespace
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
import pytest
|
|
|
|
from e2e_ui import browser_session
|
|
from test_e2e_widget_catalog import export_workflow, setting, show_node, widget_state
|
|
|
|
|
|
@pytest.mark.parametrize("restore_by_name", [False, True], ids=["positional", "named"])
|
|
def test_native_mask_tutorial_widgets(tmp_path, restore_by_name):
|
|
tutorials_dir = os.environ.get("IMPACT_TUTORIALS_DIR")
|
|
if not tutorials_dir:
|
|
pytest.fail("Set IMPACT_TUTORIALS_DIR to a ComfyUI-extension-tutorials checkout.", pytrace=False)
|
|
tutorials = Path(tutorials_dir) / "ComfyUI-Impact-Pack/workflow"
|
|
args = Namespace(base_url=os.environ.get("IMPACT_E2E_BASE_URL", "http://127.0.0.1:18188"),
|
|
browser_channel="chrome", headed=False, timeout=30,
|
|
artifacts=Path(os.environ.get("IMPACT_E2E_ARTIFACTS", str(tmp_path))) / f"tutorial-{restore_by_name}")
|
|
names = ["combined", "crop_factor", "bbox_fill", "drop_size", "contour_fill"]
|
|
cases = [
|
|
("mask_to_segs.png", 5, {"3": [False, 1, False, 50, False]}),
|
|
# Four saved values: the absent contour_fill must retain its False default.
|
|
("maskpointer.json", 4, {"13": [True, 2, False, 10, False], "57": [False, 2, False, 10, False]}),
|
|
("segs-manipulation.png", 5, {"2": [False, 3, False, 10, False]}),
|
|
]
|
|
with browser_session(args) as ui:
|
|
ui.page.keyboard.press("Escape")
|
|
setting(ui, "Restore widget values", "Comfy.Workflow.NamedValuesRestore", restore_by_name)
|
|
for filename, saved_count, expected in cases:
|
|
path = tutorials / filename
|
|
if path.suffix == ".png":
|
|
with Image.open(path) as image:
|
|
original = json.loads(image.info["workflow"])
|
|
else:
|
|
original = json.loads(path.read_text())
|
|
original_values = {str(node["id"]): node["widgets_values"] for node in original["nodes"] if str(node["id"]) in expected}
|
|
expected_original = {node_id: values[:saved_count] for node_id, values in expected.items()}
|
|
assert json.dumps(original_values, sort_keys=True) == json.dumps(expected_original, sort_keys=True), (
|
|
f"Tutorial saved controls changed: {filename}")
|
|
for loading_original in (True, False):
|
|
ui.new_workflow()
|
|
# Missing media/models do not prevent checking the saved controls.
|
|
ui.load_workflow(path, len(original["nodes"]), check_node_errors=False)
|
|
for node_id, values in expected.items():
|
|
show_node(ui, node_id)
|
|
state = widget_state(ui, node_id)
|
|
assert state["type"] == "MaskToSEGS"
|
|
wanted = [{"name": name, "value": value} for name, value in zip(names, values)]
|
|
assert json.dumps(state["widgets"], sort_keys=True) == json.dumps(wanted, sort_keys=True)
|
|
exported = args.artifacts / f"{Path(filename).stem}-{int(loading_original)}.json"
|
|
export_workflow(ui, exported)
|
|
saved = json.loads(exported.read_text())
|
|
values = {str(node["id"]): node["widgets_values"] for node in saved["nodes"] if str(node["id"]) in expected}
|
|
# Preserve JSON types: Python equality alone accepts False == 0.
|
|
assert json.dumps(values, sort_keys=True) == json.dumps(expected, sort_keys=True)
|
|
path = exported
|