Files
ltdrdata-ComfyUI-Impact-Pack/tests/test_e2e_mask_tutorials.py
T
Dr.Lt.Data 2c9efb1741 Clamp mask values before contour detection
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.
2026-11-01 12:39:49 +09:00

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