feat(ui): pattern modes, bundles, and zone content kind

Add profile/preset/sequence JSON import and export; map preset mode to
wire n6 with a mode dropdown for multi-mode patterns; zone edit shows
presets or sequences only with content_kind on save; update catalogue
and tests for merged pattern names.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-16 21:12:42 +12:00
parent 6286297646
commit 96d1e1b5fd
28 changed files with 1715 additions and 458 deletions

View File

@@ -50,6 +50,17 @@ def _find_id_by_field(list_resp_json: Dict[str, Any], field: str, value: str) ->
raise AssertionError(f"Could not find id for {field}={value!r}")
def _create_and_apply_profile(c: requests.Session, base_url: str) -> str:
"""Sequences/scenes/presets need an active profile in session."""
unique_profile_name = f"pytest-profile-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/profiles", json={"name": unique_profile_name})
assert resp.status_code == 201
profile_id = next(iter(resp.json().keys()))
resp = c.post(f"{base_url}/profiles/{profile_id}/apply")
assert resp.status_code == 200
return str(profile_id)
def _start_microdot_server(app: Microdot, host: str, port: int):
"""
Start Microdot server on a background thread.
@@ -474,6 +485,36 @@ def test_profiles_presets_zones_endpoints(server, monkeypatch):
resp = c.delete(f"{base_url}/zones/{zone_id}")
assert resp.status_code == 200
resp = c.get(f"{base_url}/profiles/{profile_id}/export")
assert resp.status_code == 200
bundle = resp.json()
assert bundle.get("kind") == "profile"
assert isinstance(bundle.get("presets"), dict)
import_name = f"pytest-imported-{uuid.uuid4().hex[:8]}"
resp = c.post(
f"{base_url}/profiles/import",
json={"bundle": bundle, "name": import_name, "apply": False},
)
assert resp.status_code == 201
imported_profile_id = resp.json().get("id") or next(
k for k in resp.json().keys() if k != "id"
)
resp = c.delete(f"{base_url}/profiles/{imported_profile_id}")
assert resp.status_code == 200
resp = c.get(f"{base_url}/presets/{first_preset_id}/export")
assert resp.status_code == 200
assert resp.json().get("kind") == "preset"
resp = c.post(
f"{base_url}/presets/import",
json={"bundle": resp.json()},
)
assert resp.status_code == 201
imported_preset_id = next(iter(resp.json().keys()))
resp = c.delete(f"{base_url}/presets/{imported_preset_id}")
assert resp.status_code == 200
# Profile clone + update endpoints.
clone_name = f"pytest-profile-clone-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/profiles/{profile_id}/clone", json={"name": clone_name})
@@ -508,6 +549,8 @@ def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
base_url: str = server["base_url"]
sender: DummySender = server["sender"]
_create_and_apply_profile(c, base_url)
# Groups.
unique_group_name = f"pytest-group-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/groups", json={"name": unique_group_name})
@@ -715,6 +758,13 @@ def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
assert resp.status_code == 200
definitions = resp.json()
assert isinstance(definitions, dict)
assert "colour_cycle" in definitions
cc_mode = definitions["colour_cycle"].get("mode")
assert isinstance(cc_mode, dict)
assert "0" in cc_mode and "1" in cc_mode
assert "blink" in definitions
blink_mode = definitions["blink"].get("mode")
assert not isinstance(blink_mode, dict) or len(blink_mode) < 2
pattern_id = f"pytest_pattern_{uuid.uuid4().hex[:8]}"
resp = c.post(