From fd618d771480b8d672ccb9f1dcfd15f4d46134eb Mon Sep 17 00:00:00 2001 From: pi Date: Mon, 6 Apr 2026 18:22:03 +1200 Subject: [PATCH] feat(zones): rename tabs to zones across api, ui, and storage Made-with: Cursor --- README.md | 8 +- db/device.json | 2 +- db/profile.json | 2 +- db/tab.json | 1 - db/zone.json | 1 + docs/API.md | 22 +- docs/SPECIFICATION.md | 10 +- docs/help.md | 36 +- docs/mockups/device-management.html | 28 +- src/controllers/device.py | 14 +- src/controllers/profile.py | 58 +- src/controllers/tab.py | 346 ------------ src/controllers/zone.py | 361 ++++++++++++ src/main.py | 6 +- src/models/device.py | 8 +- src/models/profile.py | 8 +- src/models/tab.py | 39 -- src/models/zone.py | 62 +++ src/static/app.js | 116 ++-- src/static/main.js | 38 +- src/static/presets.js | 250 ++++----- src/static/profiles.js | 6 +- src/static/style.css | 52 +- src/static/styles.css | 14 +- .../{tab_palette.js => zone_palette.js} | 36 +- src/static/{tabs.js => zones.js} | 524 +++++++++--------- src/templates/index.html | 76 +-- tests/models/run_all.py | 4 +- tests/models/test_device.py | 8 +- tests/models/test_profile.py | 8 +- tests/models/test_tab.py | 57 -- tests/models/test_zone.py | 57 ++ tests/test_browser.py | 190 +++---- tests/test_endpoints.py | 168 +++--- tests/test_endpoints_pytest.py | 34 +- 35 files changed, 1347 insertions(+), 1303 deletions(-) delete mode 100644 db/tab.json create mode 100644 db/zone.json delete mode 100644 src/controllers/tab.py create mode 100644 src/controllers/zone.py delete mode 100644 src/models/tab.py create mode 100644 src/models/zone.py rename src/static/{tab_palette.js => zone_palette.js} (88%) rename src/static/{tabs.js => zones.js} (63%) delete mode 100644 tests/models/test_tab.py create mode 100644 tests/models/test_zone.py diff --git a/README.md b/README.md index 31804cf..945e71b 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ LED controller web app for managing profiles, tabs, presets, and colour palettes ## Profiles -- Applying a profile updates session scope and refreshes the active tab content. +- Applying a profile updates session scope and refreshes the active zone content. - In **Run mode**, Profiles supports apply-only behavior (no create/clone/delete). - In **Edit mode**, Profiles supports create/clone/delete. -- Creating a profile always creates a populated `default` tab (starter presets). -- Optional **DJ tab** seeding creates: - - `dj` tab bound to device name `dj` +- Creating a profile always creates a populated `default` zone (starter presets). +- Optional **DJ zone** seeding creates: + - `dj` zone bound to device name `dj` - starter DJ presets (rainbow, single colour, transition) ## Preset colours and palette linking diff --git a/db/device.json b/db/device.json index 689e6c7..d0fba15 100644 --- a/db/device.json +++ b/db/device.json @@ -1 +1 @@ -{"aabbccddeeff": {"id": "aabbccddeeff", "name": "one", "type": "led", "transport": "espnow", "address": "aabbccddeeff", "default_pattern": null, "tabs": []}, "f0f5bdfd78b8": {"id": "f0f5bdfd78b8", "name": "a", "type": "led", "transport": "wifi", "address": "10.1.1.215", "default_pattern": null, "tabs": []}} \ No newline at end of file +{"aabbccddeeff": {"id": "aabbccddeeff", "name": "one", "type": "led", "transport": "espnow", "address": "aabbccddeeff", "default_pattern": null, "zones": []}, "f0f5bdfd78b8": {"id": "f0f5bdfd78b8", "name": "a", "type": "led", "transport": "wifi", "address": "10.1.1.215", "default_pattern": null, "zones": []}} diff --git a/db/profile.json b/db/profile.json index 11e42f8..d74ac27 100644 --- a/db/profile.json +++ b/db/profile.json @@ -1 +1 @@ -{"1": {"name": "default", "type": "tabs", "tabs": ["1", "8"], "scenes": [], "palette_id": "1"}, "2": {"name": "test", "type": "tabs", "tabs": ["6", "7"], "scenes": [], "palette_id": "12"}} \ No newline at end of file +{"1": {"name": "default", "type": "zones", "zones": ["1", "8"], "scenes": [], "palette_id": "1"}, "2": {"name": "test", "type": "zones", "zones": ["6", "7"], "scenes": [], "palette_id": "12"}} \ No newline at end of file diff --git a/db/tab.json b/db/tab.json deleted file mode 100644 index 636c57b..0000000 --- a/db/tab.json +++ /dev/null @@ -1 +0,0 @@ -{"1": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["4", "2", "7"], ["15", "3", "14"], ["5", "8", "10"], ["11", "9", "12"], ["1", "13", "37"]], "presets_flat": ["4", "2", "7", "15", "3", "14", "5", "8", "10", "11", "9", "12", "1", "13", "37"], "default_preset": "15"}, "2": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["16", "17", "18"], ["19", "20", "21"], ["22", "23", "24"], ["25", "26", "27"], ["28", "29", "30"]], "presets_flat": ["16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]}, "3": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "4": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "5": {"name": "dj", "names": ["dj"], "presets": [["31", "32", "33"]], "default_preset": "31", "presets_flat": ["31", "32", "33"]}, "6": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "7": {"name": "dj", "names": ["dj"], "presets": [["34", "35", "36"]], "default_preset": "34", "presets_flat": ["34", "35", "36"]}, "8": {"name": "test", "names": ["11"], "presets": [["1", "2", "3"], ["4", "5"]], "default_preset": "1", "presets_flat": ["1", "2", "3", "4", "5"]}} \ No newline at end of file diff --git a/db/zone.json b/db/zone.json new file mode 100644 index 0000000..efa0ad9 --- /dev/null +++ b/db/zone.json @@ -0,0 +1 @@ +{"1": {"name": "default", "names": ["e", "c", "d", "a"], "presets": [["4", "2", "7"], ["3", "14", "5"], ["8", "10", "11"], ["9", "12", "1"], ["13", "37", "6"]], "presets_flat": ["4", "2", "7", "3", "14", "5", "8", "10", "11", "9", "12", "1", "13", "37", "6"], "default_preset": "15"}, "2": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["16", "17", "18"], ["19", "20", "21"], ["22", "23", "24"], ["25", "26", "27"], ["28", "29", "30"]], "presets_flat": ["16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]}, "3": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "4": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "5": {"name": "dj", "names": ["dj"], "presets": [["31", "32", "33"]], "default_preset": "31", "presets_flat": ["31", "32", "33"]}, "6": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "7": {"name": "dj", "names": ["dj"], "presets": [["34", "35", "36"]], "default_preset": "34", "presets_flat": ["34", "35", "36"]}, "8": {"name": "test", "names": ["11"], "presets": [["1", "2", "3"], ["4", "5"]], "default_preset": "1", "presets_flat": ["1", "2", "3", "4", "5"]}} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 9e7c803..6c9c576 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,7 +15,7 @@ All JSON APIs use `Content-Type: application/json` for bodies and responses unle The main UI has two modes controlled by the mode toggle: -- **Run mode**: optimized for operation (tab/preset selection and profile apply). +- **Run mode**: optimized for operation (zone/preset selection and profile apply). - **Edit mode**: shows editing/management controls (tabs, presets, patterns, colour palette, send presets, profile management actions, **Devices** registry for LED driver names/MACs, and related tools). Profiles are available in both modes, but behavior differs: @@ -100,7 +100,7 @@ Existing records without `type` / `transport` / `id` are backfilled on load (`le | GET | `/profiles` | `{"profiles": {...}, "current_profile_id": ""}`. Ensures a default current profile when possible. | | GET | `/profiles/current` | `{"id": "...", "profile": {...}}` | | GET | `/profiles/` | Single profile. If `` is `current`, same as `/profiles/current`. | -| POST | `/profiles` | Create profile. Body may include `name` and other fields. Optional `seed_dj_tab` (request-only) seeds a DJ tab + presets. New profiles always get a populated `default` tab. Returns `{ "": { ... } }` with status 201. | +| POST | `/profiles` | Create profile. Body may include `name` and other fields. Optional `seed_dj_zone` (request-only) seeds a DJ zone + presets. New profiles always get a populated `default` zone. Returns `{ "": { ... } }` with status 201. | | POST | `/profiles//apply` | Sets session current profile to ``. | | POST | `/profiles//clone` | Clone profile (tabs, palettes, presets). Body may include `name`. | | PUT | `/profiles/current` | Update the current profile (from session). | @@ -143,18 +143,18 @@ Stored preset records can include: - `colors`: resolved hex colours for editor/display. - `palette_refs`: optional array of palette indexes parallel to `colors`. If a slot contains an integer index, the colour is linked to the current profile palette at that index. -### Tabs — `/tabs` +### Tabs — `/zones` | Method | Path | Description | |--------|------|-------------| -| GET | `/tabs` | `tabs`, `tab_order`, `current_tab_id`, `profile_id` for the session-backed profile. | -| GET | `/tabs/current` | Current tab from cookie/session. | -| POST | `/tabs` | Create tab; optional JSON `name`, `names`, `presets`; can append to current profile’s tab list. | -| GET | `/tabs/` | Tab JSON. | -| PUT | `/tabs/` | Update tab. | -| DELETE | `/tabs/` | Delete tab; can delete `current` to remove the active tab; updates profile tab list. | -| POST | `/tabs//set-current` | Sets `current_tab` cookie. | -| POST | `/tabs//clone` | Clone tab into current profile. | +| GET | `/zones` | `tabs`, `zone_order`, `current_zone_id`, `profile_id` for the session-backed profile. | +| GET | `/zones/current` | Current zone from cookie/session. | +| POST | `/zones` | Create zone; optional JSON `name`, `names`, `presets`; can append to current profile’s zone list. | +| GET | `/zones/` | Zone JSON. | +| PUT | `/zones/` | Update zone. | +| DELETE | `/zones/` | Delete zone; can delete `current` to remove the active zone; updates profile zone list. | +| POST | `/zones//set-current` | Sets `current_zone` cookie. | +| POST | `/zones//clone` | Clone zone into current profile. | ### Palettes — `/palettes` diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 6d0e46e..021655f 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -351,9 +351,9 @@ Manage connected devices and create/manage device groups. #### Layout - **Header:** Title with "Add Device" button - **Tabs:** Devices and Groups tabs -- **Content Area:** Tab-specific content +- **Content Area:** Zone-specific content -#### Devices Tab +#### Devices Zone **Device List** - **Display:** List of all known devices @@ -375,7 +375,7 @@ Manage connected devices and create/manage device groups. - **Actions:** Cancel, Save - **Note:** Only one master device per system. Adding a new master will demote existing master to slave. -#### Groups Tab +#### Groups Zone **Group List** - **Display:** List of all device groups @@ -397,7 +397,7 @@ Manage connected devices and create/manage device groups. - **Actions:** Cancel, Create #### Design Specifications -- **Tab Style:** Active tab has purple background, white text +- **Zone Style:** Active zone has purple background, white text - **List Items:** Bordered cards with hover effects - **Modal:** Centered overlay with white card, shadow - **Status Badges:** Colored pills (green for online, red for offline) @@ -1495,7 +1495,7 @@ peak_mem = usqlite.mem_peak() ### Flow 2: Create Device Group -1. User navigates to Device Management → Groups tab +1. User navigates to Device Management → Groups zone 2. User clicks "Create Group", enters name, selects pattern/settings 3. User selects devices to add (can include master), clicks "Create" 4. Group appears in list diff --git a/docs/help.md b/docs/help.md index fdf26ff..8f917c2 100644 --- a/docs/help.md +++ b/docs/help.md @@ -12,13 +12,13 @@ Figures below are **schematic** (layout and ideas), not pixel-perfect screenshot The header has a mode toggle (desktop and mobile menu). The **label on the button is the mode you switch to** when you press it. -![Schematic: tab buttons on the left; Profiles, Tabs, Presets, Patterns, and the mode toggle on the right (example shows Edit mode with “Run mode” on the button).](images/help/header-toolbar.svg) +![Schematic: zone buttons on the left; Profiles, Tabs, Presets, Patterns, and the mode toggle on the right (example shows Edit mode with “Run mode” on the button).](images/help/header-toolbar.svg) -*The active tab is highlighted. Extra management buttons appear only in Edit mode.* +*The active zone is highlighted. Extra management buttons appear only in Edit mode.* | Mode | Purpose | |------|--------| -| **Run mode** | Day-to-day control: choose a tab, tap presets, apply profiles. Management buttons are hidden. | +| **Run mode** | Day-to-day control: choose a zone, tap presets, apply profiles. Management buttons are hidden. | | **Edit mode** | Full setup: tabs, presets, patterns, colour palette, **Send Presets**, profile create/clone/delete, preset reordering, and per-tile **Edit** on the strip. | **Profiles** is available in both modes: in Run mode you can only **apply** a profile; in Edit mode you can also **create**, **clone**, and **delete** profiles. @@ -27,23 +27,23 @@ The header has a mode toggle (desktop and mobile menu). The **label on the butto ## Tabs -- **Select a tab**: click its button in the top bar. The main area shows that tab’s preset strip and controls. -- **Edit mode — open tab settings**: **right-click** a tab button to change its name, **device IDs** (comma-separated), and which presets appear on the tab. Device identifiers are matched to each device’s **name** when the app builds `select` messages for the driver. +- **Select a zone**: click its button in the top bar. The main area shows that zone’s preset strip and controls. +- **Edit mode — open zone settings**: **right-click** a zone button to change its name, **device IDs** (comma-separated), and which presets appear on the zone. Device identifiers are matched to each device’s **name** when the app builds `select` messages for the driver. - **Tabs modal** (Edit mode): create new tabs from the header **Tabs** button. New tabs need a name and device ID list (defaults to `1` if you leave a simple placeholder). -- **Brightness slider** (per tab): adjusts **global** brightness sent to devices (`b` in the driver message), with a short debounce so small drags do not flood the link. +- **Brightness slider** (per zone): adjusts **global** brightness sent to devices (`b` in the driver message), with a short debounce so small drags do not flood the link. --- -## Presets on the tab strip +## Presets on the zone strip -- **Run and Edit mode**: click the **main part** of a preset tile to **select** that preset on all devices assigned to the current tab (same logical action as a `select` in the driver API). +- **Run and Edit mode**: click the **main part** of a preset tile to **select** that preset on all devices assigned to the current zone (same logical action as a `select` in the driver API). - **Edit mode only**: - - **Edit** beside a tile opens the preset editor for that preset, scoped to the current tab (so you can **Remove from tab** without deleting the preset from the profile). - - **Drag and drop** tiles to reorder them; order is saved for that tab. + - **Edit** beside a tile opens the preset editor for that preset, scoped to the current zone (so you can **Remove from zone** without deleting the preset from the profile). + - **Drag and drop** tiles to reorder them; order is saved for that zone. -![Schematic: tab title, brightness slider, and a row of preset tiles; Edit mode adds an Edit control and drag handles for reordering.](images/help/tab-preset-strip.svg) +![Schematic: zone title, brightness slider, and a row of preset tiles; Edit mode adds an Edit control and drag handles for reordering.](images/help/zone-preset-strip.svg) -*The slider controls global brightness for the tab’s devices. Click the coloured area of a tile to select that preset.* +*The slider controls global brightness for the zone’s devices. Click the coloured area of a tile to select that preset.* The **Presets** header button (Edit mode) opens a **profile-wide** list: **Add** new presets, **Edit**, **Send** (push definition over the transport), and **Delete** (removes the preset from the profile entirely). @@ -55,10 +55,10 @@ The **Presets** header button (Edit mode) opens a **profile-wide** list: **Add** - **Colours**: choosing a value in the colour picker **adds** a swatch when the picker closes. Swatches can be **reordered** by dragging. Changing a swatch with the picker **clears** palette linkage for that slot. - **From Palette**: inserts a colour **linked** to the current profile’s palette. Linked slots show a **P** badge; if you change that palette entry later, presets using it update. - **Brightness (0–255)** and **Delay (ms)**: stored on the preset and sent with the compact preset payload. -- **Try**: sends the current form values to devices on the **current tab**, then selects that preset — **without** `save` on the device (good for auditioning). -- **Default**: updates the tab’s **default preset** and sends a **default** hint for those devices; it does not force the same live selection behaviour as clicking a tile. +- **Try**: sends the current form values to devices on the **current zone**, then selects that preset — **without** `save` on the device (good for auditioning). +- **Default**: updates the zone’s **default preset** and sends a **default** hint for those devices; it does not force the same live selection behaviour as clicking a tile. - **Save & Send**: writes the preset to the server, then pushes definitions with **save** so devices may persist them. It does **not** auto-select the preset on devices (use the strip or **Try** if you want that). -- **Remove from tab** (when you opened the editor from a tab): removes the preset from **this tab’s list only**; the preset remains in the profile for other tabs. +- **Remove from zone** (when you opened the editor from a zone): removes the preset from **this zone’s list only**; the preset remains in the profile for other zones. ![Schematic: preset editor with name, pattern, colour swatches (one with a P badge for palette-linked), and action buttons.](images/help/preset-editor.svg) @@ -69,14 +69,14 @@ The **Presets** header button (Edit mode) opens a **profile-wide** list: **Add** ## Profiles - **Apply**: sets the **current profile** in your session. Tabs and presets you see are scoped to that profile. -- **Edit mode — Create**: new profiles always get a populated **default** tab. Optionally tick **DJ tab** to also create a `dj` tab (device name `dj`) with starter DJ-oriented presets. +- **Edit mode — Create**: new profiles always get a populated **default** zone. Optionally tick **DJ zone** to also create a `dj` zone (device name `dj`) with starter DJ-oriented presets. - **Clone** / **Delete**: available in Edit mode from the profile list. --- ## Send Presets (Edit mode) -**Send Presets** walks **every tab** in the **current profile**, collects each tab’s preset IDs, and calls **`POST /presets/send`** per tab (including each tab’s **default** preset when set). Use this to bulk-push definitions to hardware after editing, without clicking **Send** on every preset individually. +**Send Presets** walks **every zone** in the **current profile**, collects each zone’s preset IDs, and calls **`POST /presets/send`** per zone (including each zone’s **default** preset when set). Use this to bulk-push definitions to hardware after editing, without clicking **Send** on every preset individually. --- @@ -102,7 +102,7 @@ On narrow screens, use **Menu** to reach the same actions as the desktop header ![Schematic: narrow layout with Menu and the same header actions in a dropdown.](images/help/mobile-menu.svg) -*Preset tiles behave the same once a tab is selected.* +*Preset tiles behave the same once a zone is selected.* --- diff --git a/docs/mockups/device-management.html b/docs/mockups/device-management.html index 48ec155..2816933 100644 --- a/docs/mockups/device-management.html +++ b/docs/mockups/device-management.html @@ -67,7 +67,7 @@ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } - .tab { + .zone { flex: 1; padding: 12px 24px; border: none; @@ -78,16 +78,16 @@ transition: all 0.2s; } - .tab.active { + .zone.active { background: #667eea; color: white; } - .tab-content { + .zone-content { display: none; } - .tab-content.active { + .zone-content.active { display: block; } @@ -249,12 +249,12 @@
- - + +
- -
+ +

Connected Devices

@@ -313,8 +313,8 @@
- -
+ +

Groups

@@ -386,12 +386,12 @@
+ - + diff --git a/tests/models/run_all.py b/tests/models/run_all.py index 8189c06..6c736e0 100644 --- a/tests/models/run_all.py +++ b/tests/models/run_all.py @@ -10,7 +10,7 @@ from test_preset import test_preset from test_profile import test_profile from test_group import test_group from test_sequence import test_sequence -from test_tab import test_tab +from test_zone import test_zone from test_palette import test_palette from test_device import test_device @@ -26,7 +26,7 @@ def run_all_tests(): ("Profile", test_profile), ("Group", test_group), ("Sequence", test_sequence), - ("Tab", test_tab), + ("Zone", test_zone), ("Palette", test_palette), ("Device", test_device), ] diff --git a/tests/models/test_device.py b/tests/models/test_device.py index 6840a3c..0f07b37 100644 --- a/tests/models/test_device.py +++ b/tests/models/test_device.py @@ -36,7 +36,7 @@ def test_device(): mac = "aabbccddeeff" print("Testing create device") - device_id = devices.create("Test Device", address="aa:bb:cc:dd:ee:ff", default_pattern="on", tabs=["1", "2"]) + device_id = devices.create("Test Device", address="aa:bb:cc:dd:ee:ff", default_pattern="on", zones=["1", "2"]) print(f"Created device with ID: {device_id}") assert device_id == mac assert device_id in devices @@ -51,7 +51,7 @@ def test_device(): assert device["transport"] == "espnow" assert device["address"] == mac assert device["default_pattern"] == "on" - assert device["tabs"] == ["1", "2"] + assert device["zones"] == ["1", "2"] print("\nTesting read by colon MAC") assert devices.read("aa:bb:cc:dd:ee:ff")["id"] == mac @@ -65,14 +65,14 @@ def test_device(): update_data = { "name": "Updated Device", "default_pattern": "rainbow", - "tabs": ["1", "2", "3"], + "zones": ["1", "2", "3"], } result = devices.update(device_id, update_data) assert result is True updated = devices.read(device_id) assert updated["name"] == "Updated Device" assert updated["default_pattern"] == "rainbow" - assert len(updated["tabs"]) == 3 + assert len(updated["zones"]) == 3 print("\nTesting list devices") device_list = devices.list() diff --git a/tests/models/test_profile.py b/tests/models/test_profile.py index 01e04b3..22ea973 100644 --- a/tests/models/test_profile.py +++ b/tests/models/test_profile.py @@ -3,7 +3,7 @@ import os def test_profile(): """Test Profile model CRUD operations. - Profile create() sets name, type, tabs (list of tab IDs), scenes, palette_id. + Profile create() sets name, type, zones (list of zone IDs), scenes, palette_id. """ # Clean up any existing test file (model uses db/profile.json from project root) db_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "db") @@ -24,20 +24,20 @@ def test_profile(): print(f"Read: {profile}") assert profile is not None assert profile["name"] == "test_profile" - assert "tabs" in profile + assert "zones" in profile assert "palette_id" in profile assert "type" in profile print("\nTesting update profile") update_data = { "name": "updated_profile", - "tabs": ["tab1"], + "zones": ["tab1"], } result = profiles.update(profile_id, update_data) assert result is True updated = profiles.read(profile_id) assert updated["name"] == "updated_profile" - assert "tab1" in updated["tabs"] + assert "tab1" in updated["zones"] print("\nTesting list profiles") profile_list = profiles.list() diff --git a/tests/models/test_tab.py b/tests/models/test_tab.py deleted file mode 100644 index 1d12868..0000000 --- a/tests/models/test_tab.py +++ /dev/null @@ -1,57 +0,0 @@ -from models.tab import Tab -import os - -def test_tab(): - """Test Tab model CRUD operations.""" - # Clean up any existing test file - if os.path.exists("Tab.json"): - os.remove("Tab.json") - - tabs = Tab() - - print("Testing create tab") - tab_id = tabs.create("test_tab", ["1", "2", "3"], ["preset1", "preset2"]) - print(f"Created tab with ID: {tab_id}") - assert tab_id is not None - assert tab_id in tabs - - print("\nTesting read tab") - tab = tabs.read(tab_id) - print(f"Read: {tab}") - assert tab is not None - assert tab["name"] == "test_tab" - assert len(tab["names"]) == 3 - assert len(tab["presets"]) == 2 - - print("\nTesting update tab") - update_data = { - "name": "updated_tab", - "names": ["4", "5"], - "presets": ["preset3"] - } - result = tabs.update(tab_id, update_data) - assert result is True - updated = tabs.read(tab_id) - assert updated["name"] == "updated_tab" - assert len(updated["names"]) == 2 - assert len(updated["presets"]) == 1 - - print("\nTesting list tabs") - tab_list = tabs.list() - print(f"Tab list: {tab_list}") - assert tab_id in tab_list - - print("\nTesting delete tab") - deleted = tabs.delete(tab_id) - assert deleted is True - assert tab_id not in tabs - - print("\nTesting read after delete") - tab = tabs.read(tab_id) - assert tab is None - - print("\nAll tab tests passed!") - - -if __name__ == '__main__': - test_tab() diff --git a/tests/models/test_zone.py b/tests/models/test_zone.py new file mode 100644 index 0000000..5436457 --- /dev/null +++ b/tests/models/test_zone.py @@ -0,0 +1,57 @@ +from models.zone import Zone +import os + + +def test_zone(): + """Test Zone model CRUD operations.""" + if os.path.exists("Zone.json"): + os.remove("Zone.json") + + zones = Zone() + + print("Testing create zone") + zone_id = zones.create("test_zone", ["1", "2", "3"], ["preset1", "preset2"]) + print(f"Created zone with ID: {zone_id}") + assert zone_id is not None + assert zone_id in zones + + print("\nTesting read zone") + zone = zones.read(zone_id) + print(f"Read: {zone}") + assert zone is not None + assert zone["name"] == "test_zone" + assert len(zone["names"]) == 3 + assert len(zone["presets"]) == 2 + + print("\nTesting update zone") + update_data = { + "name": "updated_zone", + "names": ["4", "5"], + "presets": ["preset3"], + } + result = zones.update(zone_id, update_data) + assert result is True + updated = zones.read(zone_id) + assert updated["name"] == "updated_zone" + assert len(updated["names"]) == 2 + assert len(updated["presets"]) == 1 + + print("\nTesting list zones") + zone_list = zones.list() + print(f"Zone list: {zone_list}") + assert zone_id in zone_list + + print("\nTesting delete zone") + deleted = zones.delete(zone_id) + assert deleted is True + assert zone_id not in zones + + print("\nTesting read after delete") + zone = zones.read(zone_id) + assert zone is None + + print("\nAll zone tests passed!") + + +if __name__ == "__main__": + test_zone() diff --git a/tests/test_browser.py b/tests/test_browser.py index 6ffc0ec..fa7d952 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -162,13 +162,13 @@ class BrowserTest: print(f" ⚠ Failed to cleanup preset {preset_id}: {e}") # Delete created tabs by ID - for tab_id in self.created_tabs: + for zone_id in self.created_tabs: try: - response = session.delete(f"{self.base_url}/tabs/{tab_id}") + response = session.delete(f"{self.base_url}/zones/{zone_id}") if response.status_code == 200: - print(f" ✓ Cleaned up tab: {tab_id}") + print(f" ✓ Cleaned up zone: {zone_id}") except Exception as e: - print(f" ⚠ Failed to cleanup tab {tab_id}: {e}") + print(f" ⚠ Failed to cleanup zone {zone_id}: {e}") # Delete created profiles by ID for profile_id in self.created_profiles: @@ -180,20 +180,20 @@ class BrowserTest: print(f" ⚠ Failed to cleanup profile {profile_id}: {e}") # Also try to cleanup by name pattern (in case IDs weren't tracked) - test_names = ['Browser Test Tab', 'Browser Test Profile', 'Browser Test Preset', - 'Preset 1', 'Preset 2', 'Preset 3', 'Edited Browser Tab'] + test_names = ['Browser Test Zone', 'Browser Test Profile', 'Browser Test Preset', + 'Preset 1', 'Preset 2', 'Preset 3', 'Edited Browser Zone'] # Cleanup tabs by name try: - tabs_response = session.get(f"{self.base_url}/tabs") + tabs_response = session.get(f"{self.base_url}/zones") if tabs_response.status_code == 200: tabs_data = tabs_response.json() - tabs = tabs_data.get('tabs', {}) - for tab_id, tab_data in tabs.items(): + 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: try: - session.delete(f"{self.base_url}/tabs/{tab_id}") - print(f" ✓ Cleaned up tab by name: {tab_data.get('name')}") + session.delete(f"{self.base_url}/zones/{zone_id}") + print(f" ✓ Cleaned up zone by name: {tab_data.get('name')}") except: pass except: @@ -330,11 +330,11 @@ def test_tabs_ui(browser: BrowserTest) -> bool: # Test 2: Open tabs modal total += 1 - if browser.click_element(By.ID, 'tabs-btn'): + if browser.click_element(By.ID, 'zones-btn'): print("✓ Clicked Tabs button") # Wait for modal to appear time.sleep(0.5) - modal = browser.wait_for_element(By.ID, 'tabs-modal') + modal = browser.wait_for_element(By.ID, 'zones-modal') if modal and 'active' in modal.get_attribute('class'): print("✓ Tabs modal opened") passed += 1 @@ -343,58 +343,58 @@ def test_tabs_ui(browser: BrowserTest) -> bool: else: print("✗ Failed to click Tabs button") - # Test 3: Create a tab via UI + # Test 3: Create a zone via UI total += 1 try: - # Fill in tab name - if browser.fill_input(By.ID, 'new-tab-name', 'Browser Test Tab'): - print(" ✓ Filled tab name") + # Fill in zone name + if browser.fill_input(By.ID, 'new-zone-name', 'Browser Test Zone'): + print(" ✓ Filled zone name") # Devices default from registry or placeholder name "1" # Click create button - if browser.click_element(By.ID, 'create-tab-btn'): + if browser.click_element(By.ID, 'create-zone-btn'): print(" ✓ Clicked create button") time.sleep(1) # Wait for creation - # Check if tab appears in list and extract ID - tabs_list = browser.wait_for_element(By.ID, 'tabs-list-modal') + # Check if zone appears in list and extract ID + tabs_list = browser.wait_for_element(By.ID, 'zones-list-modal') if tabs_list: list_text = tabs_list.text - if 'Browser Test Tab' in list_text: - print("✓ Created tab via UI") - # Try to extract tab ID from the list (look for data-tab-id attribute) + if 'Browser Test Zone' in list_text: + print("✓ Created zone via UI") + # Try to extract zone ID from the list (look for data-zone-id attribute) try: - tab_rows = browser.driver.find_elements(By.CSS_SELECTOR, '#tabs-list-modal .profiles-row') + tab_rows = browser.driver.find_elements(By.CSS_SELECTOR, '#zones-list-modal .profiles-row') for row in tab_rows: - if 'Browser Test Tab' in row.text: - tab_id = row.get_attribute('data-tab-id') - if tab_id: - browser.created_tabs.append(tab_id) + if 'Browser Test Zone' in row.text: + zone_id = row.get_attribute('data-zone-id') + if zone_id: + browser.created_tabs.append(zone_id) break except: pass # If we can't extract ID, cleanup will try by name passed += 1 else: - print("✗ Tab not found in list after creation") + print("✗ Zone not found in list after creation") else: print("✗ Tabs list not found") else: print("✗ Failed to click create button") except Exception as e: - print(f"✗ Failed to create tab via UI: {e}") + print(f"✗ Failed to create zone via UI: {e}") - # Test 4: Edit a tab via UI (right-click in Tabs list) + # Test 4: Edit a zone via UI (right-click in Tabs list) total += 1 try: # First, close and reopen modal to refresh - browser.click_element(By.ID, 'tabs-close-btn') + browser.click_element(By.ID, 'zones-close-btn') time.sleep(0.5) - browser.click_element(By.ID, 'tabs-btn') + browser.click_element(By.ID, 'zones-btn') time.sleep(0.5) - # Right-click the row corresponding to 'Browser Test Tab' + # Right-click the row corresponding to 'Browser Test Zone' try: tab_row = browser.driver.find_element( By.XPATH, - "//div[@id='tabs-list-modal']//div[contains(@class,'profiles-row')][.//span[contains(text(), 'Browser Test Tab')]]" + "//div[@id='zones-list-modal']//div[contains(@class,'profiles-row')][.//span[contains(text(), 'Browser Test Zone')]]" ) except Exception: tab_row = None @@ -405,14 +405,14 @@ def test_tabs_ui(browser: BrowserTest) -> bool: time.sleep(0.5) # Check if edit modal opened - edit_modal = browser.wait_for_element(By.ID, 'edit-tab-modal') + edit_modal = browser.wait_for_element(By.ID, 'edit-zone-modal') if edit_modal: print("✓ Edit modal opened via right-click") # Fill in new name - if browser.fill_input(By.ID, 'edit-tab-name', 'Edited Browser Tab'): - print(" ✓ Filled new tab name") + if browser.fill_input(By.ID, 'edit-zone-name', 'Edited Browser Zone'): + print(" ✓ Filled new zone name") # Submit form - edit_form = browser.wait_for_element(By.ID, 'edit-tab-form') + edit_form = browser.wait_for_element(By.ID, 'edit-zone-form') if edit_form: browser.driver.execute_script("arguments[0].submit();", edit_form) time.sleep(1) # Wait for update @@ -423,24 +423,24 @@ def test_tabs_ui(browser: BrowserTest) -> bool: else: print("✗ Edit modal didn't open after right-click") else: - print("✗ Could not find tab row for 'Browser Test Tab'") + print("✗ Could not find zone row for 'Browser Test Zone'") except Exception as e: - print(f"✗ Failed to edit tab via UI: {e}") + print(f"✗ Failed to edit zone via UI: {e}") import traceback traceback.print_exc() - # Test 5: Check current tab cookie + # Test 5: Check current zone cookie total += 1 - cookie = browser.get_cookie('current_tab') + cookie = browser.get_cookie('current_zone') if cookie: - print(f"✓ Found current_tab cookie: {cookie.get('value')}") + print(f"✓ Found current_zone cookie: {cookie.get('value')}") passed += 1 else: - print("⚠ No current_tab cookie found (might be normal if no tab selected)") + print("⚠ No current_zone cookie found (might be normal if no zone selected)") passed += 1 # Not a failure, just informational # Close modal - browser.click_element(By.ID, 'tabs-close-btn') + browser.click_element(By.ID, 'zones-close-btn') except Exception as e: print(f"✗ Browser test error: {e}") @@ -519,7 +519,7 @@ def test_profiles_ui(browser: BrowserTest) -> bool: def test_mobile_tab_presets_two_columns(): """ - Verify that the tab preset selecting area shows roughly two preset tiles per row + Verify that the zone preset selecting area shows roughly two preset tiles per row on a phone-sized viewport. """ bt = BrowserTest(base_url=BASE_URL, headless=True) @@ -531,18 +531,18 @@ def test_mobile_tab_presets_two_columns(): bt.driver.set_window_size(400, 800) assert bt.navigate('/'), "Failed to load main page" - # Click the first tab button to load presets for that tab - first_tab = bt.wait_for_element(By.CSS_SELECTOR, '.tab-button', timeout=10) - assert first_tab is not None, "No tab buttons found" + # Click the first zone button to load presets for that zone + first_tab = bt.wait_for_element(By.CSS_SELECTOR, '.zone-button', timeout=10) + assert first_tab is not None, "No zone buttons found" first_tab.click() time.sleep(1) - container = bt.wait_for_element(By.ID, 'presets-list-tab', timeout=10) - assert container is not None, "presets-list-tab not found" + container = bt.wait_for_element(By.ID, 'presets-list-zone', timeout=10) + assert container is not None, "presets-list-zone not found" - tiles = bt.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .preset-tile-row') + tiles = bt.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .preset-tile-row') # Need at least 2 presets to make this meaningful - assert len(tiles) >= 2, "Fewer than 2 presets found for tab" + assert len(tiles) >= 2, "Fewer than 2 presets found for zone" container_width = container.size['width'] first_width = tiles[0].size['width'] @@ -760,8 +760,8 @@ def test_color_palette_ui(browser: BrowserTest) -> bool: return passed >= total - 1 # Allow one failure (alert handling might be flaky) def test_preset_drag_and_drop(browser: BrowserTest) -> bool: - """Test dragging presets around in a tab.""" - print("\n=== Testing Preset Drag and Drop in Tab ===") + """Test dragging presets around in a zone.""" + print("\n=== Testing Preset Drag and Drop in Zone ===") passed = 0 total = 0 @@ -769,7 +769,7 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: return False try: - # Test 1: Load page and ensure we have a tab + # Test 1: Load page and ensure we have a zone total += 1 if browser.navigate('/'): print("✓ Loaded main page") @@ -778,33 +778,33 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: browser.teardown() return False - # Test 2: Open tabs modal and create/select a tab + # Test 2: Open tabs modal and create/select a zone total += 1 - browser.click_element(By.ID, 'tabs-btn') + browser.click_element(By.ID, 'zones-btn') time.sleep(0.5) # Check if we have tabs, if not create one - tabs_list = browser.wait_for_element(By.ID, 'tabs-list-modal') + tabs_list = browser.wait_for_element(By.ID, 'zones-list-modal') if tabs_list and 'No tabs found' in tabs_list.text: - # Create a tab - browser.fill_input(By.ID, 'new-tab-name', 'Drag Test Tab') - browser.click_element(By.ID, 'create-tab-btn') + # Create a zone + browser.fill_input(By.ID, 'new-zone-name', 'Drag Test Zone') + browser.click_element(By.ID, 'create-zone-btn') time.sleep(1) - # Select first tab (or the one we just created) + # Select first zone (or the one we just created) select_buttons = browser.driver.find_elements(By.XPATH, "//button[contains(text(), 'Select')]") if select_buttons: select_buttons[0].click() time.sleep(1) - print("✓ Selected a tab") + print("✓ Selected a zone") passed += 1 else: print("✗ No tabs available to select") - browser.click_element(By.ID, 'tabs-close-btn') + browser.click_element(By.ID, 'zones-close-btn') browser.teardown() return False - browser.click_element(By.ID, 'tabs-close-btn', use_js=True) + browser.click_element(By.ID, 'zones-close-btn', use_js=True) time.sleep(0.5) # Test 3: Open presets modal and create presets @@ -845,54 +845,54 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: print("✓ Created 3 presets for drag test") passed += 1 - # Test 4: Add presets to the tab (via Edit Tab modal – Add buttons in list) + # Test 4: Add presets to the zone (via Edit Zone modal – Add buttons in list) total += 1 try: - tab_id = browser.driver.execute_script( + zone_id = browser.driver.execute_script( "return (window.tabsManager && window.tabsManager.getCurrentTabId && window.tabsManager.getCurrentTabId()) || null;" ) - if not tab_id: - print("✗ Could not get current tab id") + if not zone_id: + print("✗ Could not get current zone id") else: browser.driver.execute_script( "if (window.tabsManager && window.tabsManager.openEditTabModal) { window.tabsManager.openEditTabModal(arguments[0], null); }", - tab_id + zone_id ) time.sleep(1) - list_el = browser.wait_for_element(By.ID, 'edit-tab-presets-list', timeout=5) + list_el = browser.wait_for_element(By.ID, 'edit-zone-presets-list', timeout=5) if list_el: - select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-tab-presets-list']//button[text()='Add']") + select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-zone-presets-list']//button[text()='Add']") if len(select_buttons) >= 2: browser.driver.execute_script("arguments[0].click();", select_buttons[0]) time.sleep(1.5) browser.handle_alert(accept=True, timeout=1) - select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-tab-presets-list']//button[text()='Add']") + select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-zone-presets-list']//button[text()='Add']") if len(select_buttons) >= 1: browser.driver.execute_script("arguments[0].click();", select_buttons[0]) time.sleep(1.5) browser.handle_alert(accept=True, timeout=1) - print(" ✓ Added 2 presets to tab") + print(" ✓ Added 2 presets to zone") passed += 1 elif len(select_buttons) == 1: browser.driver.execute_script("arguments[0].click();", select_buttons[0]) time.sleep(1.5) browser.handle_alert(accept=True, timeout=1) - print(" ✓ Added 1 preset to tab") + print(" ✓ Added 1 preset to zone") passed += 1 else: - print(" ⚠ No presets available to add (all already in tab)") + print(" ⚠ No presets available to add (all already in zone)") else: - print("✗ Edit tab presets list not found") + print("✗ Edit zone presets list not found") except Exception as e: - print(f"✗ Failed to add presets to tab: {e}") + print(f"✗ Failed to add presets to zone: {e}") import traceback traceback.print_exc() - # Test 5: Find presets in tab and test drag and drop (Edit mode only) + # Test 5: Find presets in zone and test drag and drop (Edit mode only) total += 1 try: - # Wait for presets to load in the tab - presets_list_tab = browser.wait_for_element(By.ID, 'presets-list-tab', timeout=5) + # Wait for presets to load in the zone + presets_list_tab = browser.wait_for_element(By.ID, 'presets-list-zone', timeout=5) if presets_list_tab: time.sleep(1) # Wait for presets to render @@ -904,7 +904,7 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: # Find draggable preset elements - wait a bit more for rendering time.sleep(1) - draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .draggable-preset') + draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .draggable-preset') if len(draggable_presets) >= 2: print(f" ✓ Found {len(draggable_presets)} draggable presets") @@ -922,7 +922,7 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: time.sleep(1) # Wait for reorder to complete # Check if order changed - draggable_presets_after = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .draggable-preset') + draggable_presets_after = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .draggable-preset') if len(draggable_presets_after) >= 2: new_order = [p.text for p in draggable_presets_after] print(f" New order: {new_order[:3]}") @@ -936,28 +936,28 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: else: print("✗ Presets disappeared after drag") elif len(draggable_presets) == 1: - print(f"⚠ Only 1 preset found in tab (need 2 for drag test). Preset: {draggable_presets[0].text}") - tab_id = browser.driver.execute_script( + print(f"⚠ Only 1 preset found in zone (need 2 for drag test). Preset: {draggable_presets[0].text}") + zone_id = browser.driver.execute_script( "return (window.tabsManager && window.tabsManager.getCurrentTabId && window.tabsManager.getCurrentTabId()) || null;" ) - if tab_id: + if zone_id: browser.driver.execute_script( "if (window.tabsManager && window.tabsManager.openEditTabModal) { window.tabsManager.openEditTabModal(arguments[0], null); }", - tab_id + zone_id ) time.sleep(1) - select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-tab-presets-list']//button[text()='Add']") + select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-zone-presets-list']//button[text()='Add']") if select_buttons: print(" Attempting to add another preset...") browser.driver.execute_script("arguments[0].click();", select_buttons[0]) time.sleep(1.5) browser.handle_alert(accept=True, timeout=1) try: - browser.driver.execute_script("document.getElementById('edit-tab-modal').classList.remove('active');") + browser.driver.execute_script("document.getElementById('edit-zone-modal').classList.remove('active');") except Exception: pass time.sleep(1) - draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .draggable-preset') + draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .draggable-preset') if len(draggable_presets) >= 2: print(" ✓ Added another preset, now testing drag...") source = draggable_presets[0] @@ -970,11 +970,11 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool: else: print(f" ✗ Still only {len(draggable_presets)} preset(s) after adding") else: - print(" ✗ No Add buttons found in Edit Tab modal") + print(" ✗ No Add buttons found in Edit Zone modal") else: - print(f"✗ No presets found in tab (found {len(draggable_presets)})") + print(f"✗ No presets found in zone (found {len(draggable_presets)})") else: - print("✗ Presets list in tab not found") + print("✗ Presets list in zone not found") except Exception as e: print(f"✗ Drag and drop test error: {e}") import traceback diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 7d39c8e..329c4d5 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -91,115 +91,115 @@ def test_tabs(client: TestClient) -> bool: # Test 1: List tabs total += 1 try: - response = client.get('/tabs') + response = client.get('/zones') if response.status_code == 200: data = response.json() - print(f"✓ GET /tabs - Found {len(data.get('tabs', {}))} tabs") + print(f"✓ GET /zones - Found {len(data.get('zones', {}))} tabs") passed += 1 else: - print(f"✗ GET /tabs - Status: {response.status_code}") + print(f"✗ GET /zones - Status: {response.status_code}") except Exception as e: - print(f"✗ GET /tabs - Error: {e}") + print(f"✗ GET /zones - Error: {e}") - # Test 2: Create tab + # Test 2: Create zone total += 1 try: tab_data = { - "name": "Test Tab", + "name": "Test Zone", "names": ["1", "2"] } - response = client.post('/tabs', json_data=tab_data) + response = client.post('/zones', json_data=tab_data) if response.status_code == 201: created_tab = response.json() - # Response format: {tab_id: {tab_data}} + # Response format: {zone_id: {tab_data}} if isinstance(created_tab, dict): - # Get the first key which should be the tab ID - tab_id = next(iter(created_tab.keys())) if created_tab else None + # Get the first key which should be the zone ID + zone_id = next(iter(created_tab.keys())) if created_tab else None else: - tab_id = None - print(f"✓ POST /tabs - Created tab: {tab_id}") + zone_id = None + print(f"✓ POST /zones - Created zone: {zone_id}") passed += 1 - # Test 3: Get specific tab - if tab_id: + # Test 3: Get specific zone + if zone_id: total += 1 - response = client.get(f'/tabs/{tab_id}') + response = client.get(f'/zones/{zone_id}') if response.status_code == 200: - print(f"✓ GET /tabs/{tab_id} - Retrieved tab") + print(f"✓ GET /zones/{zone_id} - Retrieved zone") passed += 1 else: - print(f"✗ GET /tabs/{tab_id} - Status: {response.status_code}") + print(f"✗ GET /zones/{zone_id} - Status: {response.status_code}") - # Test 4: Set current tab + # Test 4: Set current zone total += 1 - response = client.post(f'/tabs/{tab_id}/set-current') + response = client.post(f'/zones/{zone_id}/set-current') if response.status_code == 200: - print(f"✓ POST /tabs/{tab_id}/set-current - Set current tab") + print(f"✓ POST /zones/{zone_id}/set-current - Set current zone") # Check cookie was set - cookie = client.get_cookie('current_tab') - if cookie == tab_id: - print(f" ✓ Cookie 'current_tab' set to {tab_id}") + cookie = client.get_cookie('current_zone') + if cookie == zone_id: + print(f" ✓ Cookie 'current_zone' set to {zone_id}") passed += 1 else: - print(f"✗ POST /tabs/{tab_id}/set-current - Status: {response.status_code}") + print(f"✗ POST /zones/{zone_id}/set-current - Status: {response.status_code}") - # Test 5: Get current tab + # Test 5: Get current zone total += 1 - response = client.get('/tabs/current') + response = client.get('/zones/current') if response.status_code == 200: data = response.json() - if data.get('tab_id') == tab_id: - print(f"✓ GET /tabs/current - Current tab is {tab_id}") + if data.get('zone_id') == zone_id: + print(f"✓ GET /zones/current - Current zone is {zone_id}") passed += 1 else: - print(f"✗ GET /tabs/current - Wrong tab ID") + print(f"✗ GET /zones/current - Wrong zone ID") else: - print(f"✗ GET /tabs/current - Status: {response.status_code}") + print(f"✗ GET /zones/current - Status: {response.status_code}") - # Test 6: Update tab (edit functionality) + # Test 6: Update zone (edit functionality) total += 1 update_data = { - "name": "Updated Test Tab", + "name": "Updated Test Zone", "names": ["1", "2", "3"] # Update device IDs too } - response = client.put(f'/tabs/{tab_id}', json_data=update_data) + response = client.put(f'/zones/{zone_id}', json_data=update_data) if response.status_code == 200: updated = response.json() - if updated.get('name') == "Updated Test Tab" and updated.get('names') == ["1", "2", "3"]: - print(f"✓ PUT /tabs/{tab_id} - Updated tab (name and device IDs)") + if updated.get('name') == "Updated Test Zone" and updated.get('names') == ["1", "2", "3"]: + print(f"✓ PUT /zones/{zone_id} - Updated zone (name and device IDs)") passed += 1 else: - print(f"✗ PUT /tabs/{tab_id} - Update didn't work correctly") - print(f" Expected name='Updated Test Tab', got '{updated.get('name')}'") + print(f"✗ PUT /zones/{zone_id} - Update didn't work correctly") + print(f" Expected name='Updated Test Zone', got '{updated.get('name')}'") print(f" Expected names=['1','2','3'], got {updated.get('names')}") else: - print(f"✗ PUT /tabs/{tab_id} - Status: {response.status_code}, Response: {response.text}") + print(f"✗ PUT /zones/{zone_id} - Status: {response.status_code}, Response: {response.text}") # Test 6b: Verify update persisted total += 1 - response = client.get(f'/tabs/{tab_id}') + response = client.get(f'/zones/{zone_id}') if response.status_code == 200: verified = response.json() - if verified.get('name') == "Updated Test Tab": - print(f"✓ GET /tabs/{tab_id} - Verified update persisted") + if verified.get('name') == "Updated Test Zone": + print(f"✓ GET /zones/{zone_id} - Verified update persisted") passed += 1 else: - print(f"✗ GET /tabs/{tab_id} - Update didn't persist") + print(f"✗ GET /zones/{zone_id} - Update didn't persist") else: - print(f"✗ GET /tabs/{tab_id} - Status: {response.status_code}") + print(f"✗ GET /zones/{zone_id} - Status: {response.status_code}") - # Test 7: Delete tab + # Test 7: Delete zone total += 1 - response = client.delete(f'/tabs/{tab_id}') + response = client.delete(f'/zones/{zone_id}') if response.status_code == 200: - print(f"✓ DELETE /tabs/{tab_id} - Deleted tab") + print(f"✓ DELETE /zones/{zone_id} - Deleted zone") passed += 1 else: - print(f"✗ DELETE /tabs/{tab_id} - Status: {response.status_code}") + print(f"✗ DELETE /zones/{zone_id} - Status: {response.status_code}") else: - print(f"✗ POST /tabs - Status: {response.status_code}, Response: {response.text}") + print(f"✗ POST /zones - Status: {response.status_code}, Response: {response.text}") except Exception as e: - print(f"✗ POST /tabs - Error: {e}") + print(f"✗ POST /zones - Error: {e}") import traceback traceback.print_exc() @@ -409,87 +409,87 @@ def test_patterns(client: TestClient) -> bool: return passed == total def test_tab_edit_workflow(client: TestClient) -> bool: - """Test complete tab edit workflow like a browser would.""" - print("\n=== Testing Tab Edit Workflow ===") + """Test complete zone edit workflow like a browser would.""" + print("\n=== Testing Zone Edit Workflow ===") passed = 0 total = 0 - # Step 1: Create a tab to edit + # Step 1: Create a zone to edit total += 1 try: tab_data = { - "name": "Tab to Edit", + "name": "Zone to Edit", "names": ["1"] } - response = client.post('/tabs', json_data=tab_data) + response = client.post('/zones', json_data=tab_data) if response.status_code == 201: created = response.json() if isinstance(created, dict): - tab_id = next(iter(created.keys())) if created else None + zone_id = next(iter(created.keys())) if created else None else: - tab_id = None + zone_id = None - if tab_id: - print(f"✓ Created tab {tab_id} for editing") + if zone_id: + print(f"✓ Created zone {zone_id} for editing") passed += 1 - # Step 2: Get the tab to verify initial state + # Step 2: Get the zone to verify initial state total += 1 - response = client.get(f'/tabs/{tab_id}') + response = client.get(f'/zones/{zone_id}') if response.status_code == 200: original_tab = response.json() - print(f"✓ Retrieved tab - Name: '{original_tab.get('name')}', IDs: {original_tab.get('names')}") + print(f"✓ Retrieved zone - Name: '{original_tab.get('name')}', IDs: {original_tab.get('names')}") passed += 1 - # Step 3: Edit the tab (simulate browser edit form submission) + # Step 3: Edit the zone (simulate browser edit form submission) total += 1 edit_data = { - "name": "Edited Tab Name", + "name": "Edited Zone Name", "names": ["2", "3", "4"] } - response = client.put(f'/tabs/{tab_id}', json_data=edit_data) + response = client.put(f'/zones/{zone_id}', json_data=edit_data) if response.status_code == 200: edited = response.json() - if edited.get('name') == "Edited Tab Name" and edited.get('names') == ["2", "3", "4"]: - print(f"✓ PUT /tabs/{tab_id} - Successfully edited tab") + if edited.get('name') == "Edited Zone Name" and edited.get('names') == ["2", "3", "4"]: + print(f"✓ PUT /zones/{zone_id} - Successfully edited zone") print(f" New name: '{edited.get('name')}'") print(f" New device IDs: {edited.get('names')}") passed += 1 else: - print(f"✗ PUT /tabs/{tab_id} - Edit didn't work correctly") + print(f"✗ PUT /zones/{zone_id} - Edit didn't work correctly") print(f" Got: {edited}") else: - print(f"✗ PUT /tabs/{tab_id} - Status: {response.status_code}, Response: {response.text}") + print(f"✗ PUT /zones/{zone_id} - Status: {response.status_code}, Response: {response.text}") - # Step 4: Verify edit persisted by getting the tab again + # Step 4: Verify edit persisted by getting the zone again total += 1 - response = client.get(f'/tabs/{tab_id}') + response = client.get(f'/zones/{zone_id}') if response.status_code == 200: verified = response.json() - if verified.get('name') == "Edited Tab Name" and verified.get('names') == ["2", "3", "4"]: - print(f"✓ GET /tabs/{tab_id} - Verified edit persisted") + if verified.get('name') == "Edited Zone Name" and verified.get('names') == ["2", "3", "4"]: + print(f"✓ GET /zones/{zone_id} - Verified edit persisted") passed += 1 else: - print(f"✗ GET /tabs/{tab_id} - Edit didn't persist") - print(f" Expected name='Edited Tab Name', got '{verified.get('name')}'") + print(f"✗ GET /zones/{zone_id} - Edit didn't persist") + print(f" Expected name='Edited Zone Name', got '{verified.get('name')}'") print(f" Expected names=['2','3','4'], got {verified.get('names')}") else: - print(f"✗ GET /tabs/{tab_id} - Status: {response.status_code}") + print(f"✗ GET /zones/{zone_id} - Status: {response.status_code}") - # Step 5: Clean up - delete the test tab + # Step 5: Clean up - delete the test zone total += 1 - response = client.delete(f'/tabs/{tab_id}') + response = client.delete(f'/zones/{zone_id}') if response.status_code == 200: - print(f"✓ DELETE /tabs/{tab_id} - Cleaned up test tab") + print(f"✓ DELETE /zones/{zone_id} - Cleaned up test zone") passed += 1 else: - print(f"✗ DELETE /tabs/{tab_id} - Status: {response.status_code}") + print(f"✗ DELETE /zones/{zone_id} - Status: {response.status_code}") else: - print(f"✗ Failed to extract tab ID from create response") + print(f"✗ Failed to extract zone ID from create response") else: - print(f"✗ POST /tabs - Status: {response.status_code}, Response: {response.text}") + print(f"✗ POST /zones - Status: {response.status_code}, Response: {response.text}") except Exception as e: - print(f"✗ Tab edit workflow - Error: {e}") + print(f"✗ Zone edit workflow - Error: {e}") import traceback traceback.print_exc() @@ -505,7 +505,7 @@ def test_static_files(client: TestClient) -> bool: static_files = [ '/static/style.css', '/static/app.js', - '/static/tabs.js', + '/static/zones.js', '/static/presets.js', '/static/profiles.js', '/static/devices.js', @@ -544,7 +544,7 @@ def main(): # Run all tests results.append(("Tabs", test_tabs(client))) - results.append(("Tab Edit Workflow", test_tab_edit_workflow(client))) + results.append(("Zone Edit Workflow", test_tab_edit_workflow(client))) results.append(("Profiles", test_profiles(client))) results.append(("Presets", test_presets(client))) results.append(("Patterns", test_patterns(client))) diff --git a/tests/test_endpoints_pytest.py b/tests/test_endpoints_pytest.py index c156b7f..3ac7a8f 100644 --- a/tests/test_endpoints_pytest.py +++ b/tests/test_endpoints_pytest.py @@ -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.tab as models_tab # noqa: E402 + import models.zone as models_tab # 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.Tab, + models_tab.Zone, models_pallet.Palette, models_scene.Scene, models_pattern.Pattern, @@ -164,7 +164,7 @@ def server(monkeypatch, tmp_path_factory): "controllers.profile", "controllers.group", "controllers.sequence", - "controllers.tab", + "controllers.zone", "controllers.palette", "controllers.scene", "controllers.pattern", @@ -178,7 +178,7 @@ def server(monkeypatch, tmp_path_factory): import controllers.profile as profile_ctl # noqa: E402 import controllers.group as group_ctl # noqa: E402 import controllers.sequence as sequence_ctl # noqa: E402 - import controllers.tab as tab_ctl # noqa: E402 + import controllers.zone as zone_ctl # noqa: E402 import controllers.palette as palette_ctl # noqa: E402 import controllers.scene as scene_ctl # noqa: E402 import controllers.pattern as pattern_ctl # noqa: E402 @@ -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, "/tabs") + app.mount(tab_ctl.controller, "/zones") app.mount(palette_ctl.controller, "/palettes") app.mount(scene_ctl.controller, "/scenes") app.mount(pattern_ctl.controller, "/patterns") @@ -424,45 +424,45 @@ def test_profiles_presets_tabs_endpoints(server, monkeypatch): assert resp.status_code == 404 # Tabs CRUD (scoped to current profile session). - unique_tab_name = f"pytest-tab-{uuid.uuid4().hex[:8]}" + unique_tab_name = f"pytest-zone-{uuid.uuid4().hex[:8]}" resp = c.post( - f"{base_url}/tabs", + f"{base_url}/zones", json={"name": unique_tab_name, "names": ["1", "2"]}, ) assert resp.status_code == 201 created_tabs = resp.json() - tab_id = next(iter(created_tabs.keys())) + zone_id = next(iter(created_tabs.keys())) - resp = c.get(f"{base_url}/tabs/{tab_id}") + resp = c.get(f"{base_url}/zones/{zone_id}") assert resp.status_code == 200 assert resp.json()["name"] == unique_tab_name - resp = c.post(f"{base_url}/tabs/{tab_id}/set-current") + resp = c.post(f"{base_url}/zones/{zone_id}/set-current") assert resp.status_code == 200 - resp = c.get(f"{base_url}/tabs/current") + resp = c.get(f"{base_url}/zones/current") assert resp.status_code == 200 - assert resp.json()["tab_id"] == str(tab_id) + assert resp.json()["zone_id"] == str(zone_id) resp = c.put( - f"{base_url}/tabs/{tab_id}", + f"{base_url}/zones/{zone_id}", json={"name": f"{unique_tab_name}-updated", "names": ["3"]}, ) assert resp.status_code == 200 assert resp.json()["names"] == ["3"] - resp = c.post(f"{base_url}/tabs/{tab_id}/clone", json={"name": "pytest-tab-clone"}) + resp = c.post(f"{base_url}/zones/{zone_id}/clone", json={"name": "pytest-zone-clone"}) assert resp.status_code == 201 clone_payload = resp.json() clone_id = next(iter(clone_payload.keys())) - resp = c.get(f"{base_url}/tabs/{clone_id}") + resp = c.get(f"{base_url}/zones/{clone_id}") assert resp.status_code == 200 - resp = c.delete(f"{base_url}/tabs/{clone_id}") + resp = c.delete(f"{base_url}/zones/{clone_id}") assert resp.status_code == 200 - resp = c.delete(f"{base_url}/tabs/{tab_id}") + resp = c.delete(f"{base_url}/zones/{zone_id}") assert resp.status_code == 200 # Profile clone + update endpoints.