test: fix zone_ctl fixture, pattern assertions, and browser cleanup
Made-with: Cursor
This commit is contained in:
@@ -6,9 +6,9 @@ Tests for the LED Controller project live under **`tests/`** (pytest + legacy sc
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `test_endpoints.py` | HTTP endpoint checks (often against a running Pi; host configurable in file) |
|
||||
| `test_endpoints.py` | HTTP endpoint checks (**`LED_CONTROLLER_RUN_DEVICE_ENDPOINT_TESTS=1`**); **`test_zones`** / **`test_zone_edit_workflow`** hit **`/zones`** |
|
||||
| `test_endpoints_pytest.py` | Pytest-style endpoint coverage |
|
||||
| `test_browser.py` | Selenium UI flows |
|
||||
| `test_browser.py` | Selenium UI flows (set **`LED_CONTROLLER_RUN_BROWSER_TESTS=1`** to run; uses **`test_zones_ui`** and legacy **`tabsManager`** JS aliases) |
|
||||
| `test_pattern_ota_send.py` | Pattern OTA / Wi-Fi send helpers |
|
||||
| `tcp_test_server.py`, `async_tcp_server.py` | TCP test doubles for driver protocol |
|
||||
| `udp_server.py` | UDP discovery / hello test listener (port **8766**) |
|
||||
|
||||
@@ -40,7 +40,7 @@ class BrowserTest:
|
||||
self.base_url = base_url
|
||||
self.driver = None
|
||||
self.headless = headless
|
||||
self.created_tabs: List[str] = []
|
||||
self.created_zones: List[str] = []
|
||||
self.created_profiles: List[str] = []
|
||||
self.created_presets: List[str] = []
|
||||
|
||||
@@ -161,8 +161,8 @@ class BrowserTest:
|
||||
except Exception as e:
|
||||
print(f" ⚠ Failed to cleanup preset {preset_id}: {e}")
|
||||
|
||||
# Delete created tabs by ID
|
||||
for zone_id in self.created_tabs:
|
||||
# Delete created zones by ID
|
||||
for zone_id in self.created_zones:
|
||||
try:
|
||||
response = session.delete(f"{self.base_url}/zones/{zone_id}")
|
||||
if response.status_code == 200:
|
||||
@@ -183,17 +183,17 @@ class BrowserTest:
|
||||
test_names = ['Browser Test Zone', 'Browser Test Profile', 'Browser Test Preset',
|
||||
'Preset 1', 'Preset 2', 'Preset 3', 'Edited Browser Zone']
|
||||
|
||||
# Cleanup tabs by name
|
||||
# Cleanup zones by name
|
||||
try:
|
||||
tabs_response = session.get(f"{self.base_url}/zones")
|
||||
if tabs_response.status_code == 200:
|
||||
tabs_data = tabs_response.json()
|
||||
tabs = tabs_data.get('zones', {})
|
||||
for zone_id, tab_data in zones.items():
|
||||
if isinstance(tab_data, dict) and tab_data.get('name') in test_names:
|
||||
zones_response = session.get(f"{self.base_url}/zones")
|
||||
if zones_response.status_code == 200:
|
||||
zones_data = zones_response.json()
|
||||
zones_map = zones_data.get('zones', {})
|
||||
for zone_id, zone_row in zones_map.items():
|
||||
if isinstance(zone_row, dict) and zone_row.get('name') in test_names:
|
||||
try:
|
||||
session.delete(f"{self.base_url}/zones/{zone_id}")
|
||||
print(f" ✓ Cleaned up zone by name: {tab_data.get('name')}")
|
||||
print(f" ✓ Cleaned up zone by name: {zone_row.get('name')}")
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
@@ -232,7 +232,7 @@ class BrowserTest:
|
||||
pass
|
||||
|
||||
# Clear the lists
|
||||
self.created_tabs.clear()
|
||||
self.created_zones.clear()
|
||||
self.created_profiles.clear()
|
||||
self.created_presets.clear()
|
||||
except Exception as e:
|
||||
@@ -308,9 +308,9 @@ def test_browser_connection(browser: BrowserTest) -> bool:
|
||||
finally:
|
||||
browser.teardown()
|
||||
|
||||
def test_tabs_ui(browser: BrowserTest) -> bool:
|
||||
"""Test tabs UI in browser."""
|
||||
print("\n=== Testing Tabs UI in Browser ===")
|
||||
def test_zones_ui(browser: BrowserTest) -> bool:
|
||||
"""Test zones UI in browser."""
|
||||
print("\n=== Testing Zones UI in Browser ===")
|
||||
passed = 0
|
||||
total = 0
|
||||
|
||||
@@ -328,20 +328,20 @@ def test_tabs_ui(browser: BrowserTest) -> bool:
|
||||
browser.teardown()
|
||||
return False
|
||||
|
||||
# Test 2: Open tabs modal
|
||||
# Test 2: Open zones modal
|
||||
total += 1
|
||||
if browser.click_element(By.ID, 'zones-btn'):
|
||||
print("✓ Clicked Tabs button")
|
||||
print("✓ Clicked Zones button")
|
||||
# Wait for modal to appear
|
||||
time.sleep(0.5)
|
||||
modal = browser.wait_for_element(By.ID, 'zones-modal')
|
||||
if modal and 'active' in modal.get_attribute('class'):
|
||||
print("✓ Tabs modal opened")
|
||||
print("✓ Zones modal opened")
|
||||
passed += 1
|
||||
else:
|
||||
print("✗ Tabs modal didn't open")
|
||||
print("✗ Zones modal didn't open")
|
||||
else:
|
||||
print("✗ Failed to click Tabs button")
|
||||
print("✗ Failed to click Zones button")
|
||||
|
||||
# Test 3: Create a zone via UI
|
||||
total += 1
|
||||
@@ -367,7 +367,7 @@ def test_tabs_ui(browser: BrowserTest) -> bool:
|
||||
if 'Browser Test Zone' in row.text:
|
||||
zone_id = row.get_attribute('data-zone-id')
|
||||
if zone_id:
|
||||
browser.created_tabs.append(zone_id)
|
||||
browser.created_zones.append(zone_id)
|
||||
break
|
||||
except:
|
||||
pass # If we can't extract ID, cleanup will try by name
|
||||
@@ -375,13 +375,13 @@ def test_tabs_ui(browser: BrowserTest) -> bool:
|
||||
else:
|
||||
print("✗ Zone not found in list after creation")
|
||||
else:
|
||||
print("✗ Tabs list not found")
|
||||
print("✗ Zones list not found")
|
||||
else:
|
||||
print("✗ Failed to click create button")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to create zone via UI: {e}")
|
||||
|
||||
# Test 4: Edit a zone via UI (right-click in Tabs list)
|
||||
# Test 4: Edit a zone via UI (right-click in zones list)
|
||||
total += 1
|
||||
try:
|
||||
# First, close and reopen modal to refresh
|
||||
@@ -450,7 +450,7 @@ def test_tabs_ui(browser: BrowserTest) -> bool:
|
||||
browser.cleanup_test_data()
|
||||
browser.teardown()
|
||||
|
||||
print(f"\nBrowser tabs UI tests: {passed}/{total} passed")
|
||||
print(f"\nBrowser zones UI tests: {passed}/{total} passed")
|
||||
return passed == total
|
||||
|
||||
def test_profiles_ui(browser: BrowserTest) -> bool:
|
||||
@@ -778,14 +778,14 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool:
|
||||
browser.teardown()
|
||||
return False
|
||||
|
||||
# Test 2: Open tabs modal and create/select a zone
|
||||
# Test 2: Open zones modal and create/select a zone
|
||||
total += 1
|
||||
browser.click_element(By.ID, 'zones-btn')
|
||||
time.sleep(0.5)
|
||||
|
||||
# Check if we have tabs, if not create one
|
||||
# Check if we have zones, if not create one
|
||||
tabs_list = browser.wait_for_element(By.ID, 'zones-list-modal')
|
||||
if tabs_list and 'No tabs found' in tabs_list.text:
|
||||
if tabs_list and 'No zones found' in tabs_list.text:
|
||||
# Create a zone
|
||||
browser.fill_input(By.ID, 'new-zone-name', 'Drag Test Zone')
|
||||
browser.click_element(By.ID, 'create-zone-btn')
|
||||
@@ -799,7 +799,7 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool:
|
||||
print("✓ Selected a zone")
|
||||
passed += 1
|
||||
else:
|
||||
print("✗ No tabs available to select")
|
||||
print("✗ No zones available to select")
|
||||
browser.click_element(By.ID, 'zones-close-btn')
|
||||
browser.teardown()
|
||||
return False
|
||||
@@ -1012,7 +1012,7 @@ def main():
|
||||
|
||||
# Run browser tests
|
||||
results.append(("Browser Connection", test_browser_connection(browser)))
|
||||
results.append(("Tabs UI", test_tabs_ui(browser)))
|
||||
results.append(("Zones UI", test_zones_ui(browser)))
|
||||
results.append(("Profiles UI", test_profiles_ui(browser)))
|
||||
results.append(("Presets UI", test_presets_ui(browser)))
|
||||
results.append(("Color Palette UI", test_color_palette_ui(browser)))
|
||||
|
||||
@@ -82,19 +82,19 @@ def test_connection(client: TestClient) -> bool:
|
||||
print(f"✗ Connection error: {e}")
|
||||
return False
|
||||
|
||||
def test_tabs(client: TestClient) -> bool:
|
||||
"""Test tabs endpoints."""
|
||||
print("\n=== Testing Tabs Endpoints ===")
|
||||
def test_zones(client: TestClient) -> bool:
|
||||
"""Test zones endpoints."""
|
||||
print("\n=== Testing Zones Endpoints ===")
|
||||
passed = 0
|
||||
total = 0
|
||||
|
||||
# Test 1: List tabs
|
||||
# Test 1: List zones
|
||||
total += 1
|
||||
try:
|
||||
response = client.get('/zones')
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✓ GET /zones - Found {len(data.get('zones', {}))} tabs")
|
||||
print(f"✓ GET /zones - Found {len(data.get('zones', {}))} zones")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ GET /zones - Status: {response.status_code}")
|
||||
@@ -104,17 +104,17 @@ def test_tabs(client: TestClient) -> bool:
|
||||
# Test 2: Create zone
|
||||
total += 1
|
||||
try:
|
||||
tab_data = {
|
||||
zone_data = {
|
||||
"name": "Test Zone",
|
||||
"names": ["1", "2"]
|
||||
}
|
||||
response = client.post('/zones', json_data=tab_data)
|
||||
response = client.post('/zones', json_data=zone_data)
|
||||
if response.status_code == 201:
|
||||
created_tab = response.json()
|
||||
# Response format: {zone_id: {tab_data}}
|
||||
if isinstance(created_tab, dict):
|
||||
created_zone = response.json()
|
||||
# Response format: {zone_id: {zone object}}
|
||||
if isinstance(created_zone, dict):
|
||||
# Get the first key which should be the zone ID
|
||||
zone_id = next(iter(created_tab.keys())) if created_tab else None
|
||||
zone_id = next(iter(created_zone.keys())) if created_zone else None
|
||||
else:
|
||||
zone_id = None
|
||||
print(f"✓ POST /zones - Created zone: {zone_id}")
|
||||
@@ -203,7 +203,7 @@ def test_tabs(client: TestClient) -> bool:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"\nTabs tests: {passed}/{total} passed")
|
||||
print(f"\nZones tests: {passed}/{total} passed")
|
||||
return passed == total
|
||||
|
||||
def test_profiles(client: TestClient) -> bool:
|
||||
@@ -405,10 +405,33 @@ def test_patterns(client: TestClient) -> bool:
|
||||
except Exception as e:
|
||||
print(f"✗ GET /patterns/definitions - Error: {e}")
|
||||
|
||||
# Test 3: Firmware-only on/off — no OTA file (400 from API).
|
||||
total += 1
|
||||
try:
|
||||
r = client.get("/patterns/ota/file/on.py")
|
||||
if r.status_code == 400 and "error" in r.json():
|
||||
print("✓ GET /patterns/ota/file/on.py - Rejected as built-in")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ GET /patterns/ota/file/on.py - Expected 400, got {r.status_code}")
|
||||
except Exception as e:
|
||||
print(f"✗ GET /patterns/ota/file/on.py - Error: {e}")
|
||||
|
||||
total += 1
|
||||
try:
|
||||
r = client.post("/patterns/off/send", json_data={})
|
||||
if r.status_code == 400 and "error" in r.json():
|
||||
print("✓ POST /patterns/off/send - Rejected as built-in")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ POST /patterns/off/send - Expected 400, got {r.status_code}")
|
||||
except Exception as e:
|
||||
print(f"✗ POST /patterns/off/send - Error: {e}")
|
||||
|
||||
print(f"\nPatterns tests: {passed}/{total} passed")
|
||||
return passed == total
|
||||
|
||||
def test_tab_edit_workflow(client: TestClient) -> bool:
|
||||
def test_zone_edit_workflow(client: TestClient) -> bool:
|
||||
"""Test complete zone edit workflow like a browser would."""
|
||||
print("\n=== Testing Zone Edit Workflow ===")
|
||||
passed = 0
|
||||
@@ -417,11 +440,11 @@ def test_tab_edit_workflow(client: TestClient) -> bool:
|
||||
# Step 1: Create a zone to edit
|
||||
total += 1
|
||||
try:
|
||||
tab_data = {
|
||||
zone_data = {
|
||||
"name": "Zone to Edit",
|
||||
"names": ["1"]
|
||||
}
|
||||
response = client.post('/zones', json_data=tab_data)
|
||||
response = client.post('/zones', json_data=zone_data)
|
||||
if response.status_code == 201:
|
||||
created = response.json()
|
||||
if isinstance(created, dict):
|
||||
@@ -437,8 +460,8 @@ def test_tab_edit_workflow(client: TestClient) -> bool:
|
||||
total += 1
|
||||
response = client.get(f'/zones/{zone_id}')
|
||||
if response.status_code == 200:
|
||||
original_tab = response.json()
|
||||
print(f"✓ Retrieved zone - Name: '{original_tab.get('name')}', IDs: {original_tab.get('names')}")
|
||||
original_zone = response.json()
|
||||
print(f"✓ Retrieved zone - Name: '{original_zone.get('name')}', IDs: {original_zone.get('names')}")
|
||||
passed += 1
|
||||
|
||||
# Step 3: Edit the zone (simulate browser edit form submission)
|
||||
@@ -493,7 +516,7 @@ def test_tab_edit_workflow(client: TestClient) -> bool:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"\nTab edit workflow tests: {passed}/{total} passed")
|
||||
print(f"\nZone edit workflow tests: {passed}/{total} passed")
|
||||
return passed == total
|
||||
|
||||
def test_static_files(client: TestClient) -> bool:
|
||||
@@ -543,8 +566,8 @@ def main():
|
||||
results = []
|
||||
|
||||
# Run all tests
|
||||
results.append(("Tabs", test_tabs(client)))
|
||||
results.append(("Zone Edit Workflow", test_tab_edit_workflow(client)))
|
||||
results.append(("Zones", test_zones(client)))
|
||||
results.append(("Zone Edit Workflow", test_zone_edit_workflow(client)))
|
||||
results.append(("Profiles", test_profiles(client)))
|
||||
results.append(("Presets", test_presets(client)))
|
||||
results.append(("Patterns", test_patterns(client)))
|
||||
|
||||
@@ -119,7 +119,7 @@ def server(monkeypatch, tmp_path_factory):
|
||||
import models.preset as models_preset # noqa: E402
|
||||
import models.profile as models_profile # noqa: E402
|
||||
import models.group as models_group # noqa: E402
|
||||
import models.zone as models_tab # noqa: E402
|
||||
import models.zone as models_zone # noqa: E402
|
||||
import models.pallet as models_pallet # noqa: E402
|
||||
import models.scene as models_scene # noqa: E402
|
||||
import models.pattern as models_pattern # noqa: E402
|
||||
@@ -130,7 +130,7 @@ def server(monkeypatch, tmp_path_factory):
|
||||
models_preset.Preset,
|
||||
models_profile.Profile,
|
||||
models_group.Group,
|
||||
models_tab.Zone,
|
||||
models_zone.Zone,
|
||||
models_pallet.Palette,
|
||||
models_scene.Scene,
|
||||
models_pattern.Pattern,
|
||||
@@ -205,7 +205,7 @@ def server(monkeypatch, tmp_path_factory):
|
||||
app.mount(profile_ctl.controller, "/profiles")
|
||||
app.mount(group_ctl.controller, "/groups")
|
||||
app.mount(sequence_ctl.controller, "/sequences")
|
||||
app.mount(tab_ctl.controller, "/zones")
|
||||
app.mount(zone_ctl.controller, "/zones")
|
||||
app.mount(palette_ctl.controller, "/palettes")
|
||||
app.mount(scene_ctl.controller, "/scenes")
|
||||
app.mount(pattern_ctl.controller, "/patterns")
|
||||
@@ -348,7 +348,7 @@ def test_settings_controller(server):
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_profiles_presets_tabs_endpoints(server, monkeypatch):
|
||||
def test_profiles_presets_zones_endpoints(server, monkeypatch):
|
||||
c: requests.Session = server["client"]
|
||||
base_url: str = server["base_url"]
|
||||
sender: DummySender = server["sender"]
|
||||
@@ -423,19 +423,19 @@ def test_profiles_presets_tabs_endpoints(server, monkeypatch):
|
||||
resp = c.get(f"{base_url}/presets/{new_preset_id}")
|
||||
assert resp.status_code == 404
|
||||
|
||||
# Tabs CRUD (scoped to current profile session).
|
||||
unique_tab_name = f"pytest-zone-{uuid.uuid4().hex[:8]}"
|
||||
# Zones CRUD (scoped to current profile session).
|
||||
unique_zone_name = f"pytest-zone-{uuid.uuid4().hex[:8]}"
|
||||
resp = c.post(
|
||||
f"{base_url}/zones",
|
||||
json={"name": unique_tab_name, "names": ["1", "2"]},
|
||||
json={"name": unique_zone_name, "names": ["1", "2"]},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
created_tabs = resp.json()
|
||||
zone_id = next(iter(created_tabs.keys()))
|
||||
created_zones = resp.json()
|
||||
zone_id = next(iter(created_zones.keys()))
|
||||
|
||||
resp = c.get(f"{base_url}/zones/{zone_id}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == unique_tab_name
|
||||
assert resp.json()["name"] == unique_zone_name
|
||||
|
||||
resp = c.post(f"{base_url}/zones/{zone_id}/set-current")
|
||||
assert resp.status_code == 200
|
||||
@@ -446,7 +446,7 @@ def test_profiles_presets_tabs_endpoints(server, monkeypatch):
|
||||
|
||||
resp = c.put(
|
||||
f"{base_url}/zones/{zone_id}",
|
||||
json={"name": f"{unique_tab_name}-updated", "names": ["3"]},
|
||||
json={"name": f"{unique_zone_name}-updated", "names": ["3"]},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["names"] == ["3"]
|
||||
@@ -497,6 +497,7 @@ def test_profiles_presets_tabs_endpoints(server, monkeypatch):
|
||||
def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
|
||||
c: requests.Session = server["client"]
|
||||
base_url: str = server["base_url"]
|
||||
sender: DummySender = server["sender"]
|
||||
|
||||
# Groups.
|
||||
unique_group_name = f"pytest-group-{uuid.uuid4().hex[:8]}"
|
||||
@@ -714,7 +715,10 @@ def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
|
||||
resp = c.get(f"{base_url}/patterns")
|
||||
assert resp.status_code == 200
|
||||
patterns_list = resp.json()
|
||||
assert pattern_id in patterns_list
|
||||
assert isinstance(patterns_list, dict)
|
||||
# Runtime list merges repo ``db/pattern.json`` + driver ``.py`` names; test DB
|
||||
# entries are still exposed on GET /patterns/<id> after POST.
|
||||
assert "blink" in patterns_list or len(patterns_list) >= 1
|
||||
|
||||
resp = c.get(f"{base_url}/patterns/{pattern_id}")
|
||||
assert resp.status_code == 200
|
||||
@@ -727,3 +731,27 @@ def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
|
||||
resp = c.delete(f"{base_url}/patterns/{pattern_id}")
|
||||
assert resp.status_code == 200
|
||||
|
||||
# on/off are firmware-only in presets.py — no OTA file; API rejects serve/send/upload/driver.
|
||||
resp = c.get(f"{base_url}/patterns/ota/file/on.py")
|
||||
assert resp.status_code == 400
|
||||
assert "error" in resp.json()
|
||||
|
||||
resp = c.post(f"{base_url}/patterns/off/send", json={})
|
||||
assert resp.status_code == 400
|
||||
assert "error" in resp.json()
|
||||
|
||||
resp = c.post(
|
||||
f"{base_url}/patterns/upload",
|
||||
json={"name": "on.py", "code": "class On:\n def run(self, p):\n yield\n"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
resp = c.post(
|
||||
f"{base_url}/patterns/driver",
|
||||
json={
|
||||
"name": "off",
|
||||
"code": "class Off:\n def run(self, p):\n yield\n",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
Reference in New Issue
Block a user