Backlog cleanup

This commit is contained in:
Claudio Ortolina
2026-05-19 09:48:55 +01:00
parent 54b682a663
commit 0881971635
11 changed files with 0 additions and 0 deletions
@@ -1,29 +0,0 @@
---
id: ML-182.1
title: >-
Wave 1: Drop unwrap/2 bridges in scrobble_live and scrobbled_tracks_live index
tests
status: Done
assignee: []
created_date: "2026-05-14 22:13"
labels:
- testing
- refactoring
dependencies: []
modified_files:
- test/music_library_web/live/scrobble_live/index_test.exs
- test/music_library_web/live/scrobbled_tracks_live/index_test.exs
parent_task_id: ML-182
priority: medium
ordinal: 11000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Remove `import Phoenix.LiveViewTest` and eliminate `unwrap/2` bridge patterns in these two files. Both files used `unwrap(fn view -> form(...) |> render_submit(); render(view) end)` to drive search forms. Conversion strategy: visit with query params directly, which triggers `handle_params` and the search — same effect as form submission and simpler.
Also clean up unused module attributes and convert validation-error tests to `fill_in` + `click_button` + `assert_has`.
<!-- SECTION:DESCRIPTION:END -->
@@ -1,25 +0,0 @@
---
id: ML-182.2
title: "Wave 2: Convert page_title/1 tests in release_group_show and release_show"
status: Done
assignee: []
created_date: "2026-05-14 22:14"
labels:
- testing
- refactoring
dependencies: []
modified_files:
- test/music_library_web/live/scrobble_live/release_group_show_test.exs
- test/music_library_web/live/scrobble_live/release_show_test.exs
parent_task_id: ML-182
priority: medium
ordinal: 12000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Replace `Phoenix.LiveViewTest.live/2` + `render_async/1` + `page_title/1` with `visit/2` + `assert_has("title", text: "...")`. Keep `unwrap(&render_async/1)` where needed for async data loading (auto-imported by ConnCase).
<!-- SECTION:DESCRIPTION:END -->
@@ -1,43 +0,0 @@
---
id: ML-182.3
title: "Wave 3: Convert live/2 → visit/2 for CRUD LiveViews (6 files)"
status: Done
assignee: []
created_date: "2026-05-14 22:14"
updated_date: "2026-05-16 14:23"
labels:
- testing
- refactoring
dependencies: []
modified_files:
- test/music_library_web/live/maintenance_live/index_test.exs
- test/music_library_web/live/online_store_template_live/index_test.exs
- test/music_library_web/live/record_set_live/index_test.exs
- test/music_library_web/live/record_set_live/show_test.exs
- test/music_library_web/live/scrobble_rules_live/index_test.exs
- test/music_library_web/live/scrobbled_tracks_live/rule_picker_test.exs
parent_task_id: ML-182
priority: medium
ordinal: 13000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Convert `live/2``visit/2` for standard CRUD LiveViews. Files:
- **maintenance_live/index_test.exs** ✅ Done (14 tests pass)
- **online_store_template_live/index_test.exs** ✅ Done (7 tests pass)
- **record_set_live/index_test.exs** ✅ Done (7 tests pass)
- **record_set_live/show_test.exs** ✅ Done (15 tests pass)
- **scrobble_rules_live/index_test.exs** ✅ Done (10 tests pass) — removed `import Phoenix.LiveViewTest`, added explicit `only:` for `render_change/1` and `form/3`. Fluxon `<.select>` component requires `unwrap` with `form/3` + `render_change/1` for type changes — standard `PhoenixTest.select/3` doesn't work with custom selects. Dynamic label ("Match Value"/"Album Title"/"Artist Name") handled via `set_rule_type` helper. Edit validation test uses `unwrap` with `render_change` to clear existing data before checking errors. Path assertions on patch links omit query params (not captured by PhoenixTest). Buttons: "Add" (link), "Save Rule", "Apply", "Apply rule", "Delete", "Disable rule"/"Enable rule".
- **scrobbled_tracks_live/rule_picker_test.exs** ⏸️ Blocked: clicks `<span>` badges with `phx-click` (Fluxon). PhoenixTest only supports `<a>` and `<button>`.
Key learnings for remaining files:
- Fluxon custom `<.select>` components are NOT native `<select>` elements — use `unwrap` with `form/3` + `render_change/1` instead of `PhoenixTest.select/3`
- For edit-form validation tests where existing data would pass validation, `unwrap` with `render_change` clears fields first
- `assert_path` on patch links doesn't capture query params — use path-only assertions
- `<li phx-click>` needs `unwrap` with `element/2` + `render_click/1` since PhoenixTest only supports `<a>` and `<button>` natively
<!-- SECTION:DESCRIPTION:END -->
@@ -1,28 +0,0 @@
---
id: ML-182.4
title: "Wave 4: Convert mixed-usage files (artist_live/show, stats_live/top_albums)"
status: Done
assignee: []
created_date: "2026-05-14 22:14"
updated_date: "2026-05-16 15:30"
labels:
- testing
- refactoring
dependencies: []
modified_files:
- test/music_library_web/live/artist_live/show_test.exs
- test/music_library_web/live/stats_live/top_albums_test.exs
parent_task_id: ML-182
priority: medium
ordinal: 14000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Convert mixed-usage files where some tests use `visit/2` and others use `live/2`.
- **artist_live/show_test.exs**: Most tests already use `visit/2` + `unwrap(&render_async/1)`. 4 tests in "Edit artist image" describe use `live/2` with `file_input/3`, `render_upload/3`, `render_submit/1`, `render_click/3`. PhoenixTest has `upload/3` equivalent. Need to verify field labels in artist image edit form.
- **stats_live/top_albums_test.exs**: Mixed `visit/2` and `live/2`. Tests using `live/2` check cover URL swapping and badge variants. Need to verify these assertions work with `visit/2` + `assert_has` after `unwrap(&render_async/1)`.
<!-- SECTION:DESCRIPTION:END -->
@@ -1,82 +0,0 @@
---
id: ML-182.5
title: "Wave 5: Files with send(view.pid) and live_isolated blockers (4 files)"
status: Done
assignee:
- Codex
created_date: "2026-05-14 22:15"
updated_date: "2026-05-17 20:18"
labels:
- testing
- refactoring
dependencies: []
modified_files:
- test/music_library_web/live/collection_live/index_test.exs
- test/music_library_web/live/wishlist_live/index_test.exs
- test/music_library_web/live_helpers/record_actions_test.exs
- test/music_library_web/components/release_test.exs
parent_task_id: ML-182
priority: medium
ordinal: 15000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Files blocked by `send(view.pid, message)` pattern for testing internal `handle_info/2` callbacks, plus `live_isolated/3` and `render_change/2` with nested LiveComponent form data.
- **collection_live/index_test.exs**: 2 PubSub tests use `send(view.pid, :records_index_changed)`. 3 cart format-change tests use `live/2` with nested `render_change/2` params. Workaround needed: trigger actual PubSub broadcasts or use alternative approach for PID message testing.
- **wishlist_live/index_test.exs**: Same PubSub pattern (2 tests). Single-item import test uses `live/2` for synchronous import flow.
- **record_actions_test.exs**: One test uses `send(view.pid, {Chat, :chats_changed})`. Rest are standard `live/2`+`render_click/3`.
- **components/release_test.exs**: `render_change/2` with nested LiveComponent form data (`%{"release" => %{"finished_at" => ...}}`). `ShowPrintTest` sub-module uses `live_isolated/3`. Workaround: test through parent LiveView page.
Blocking patterns:
1. `send(view.pid, message)` — PhoenixTest sessions don't expose LiveView PIDs
2. `live_isolated/3` — no PhoenixTest equivalent; test through parent page
3. `render_change/2` with nested params — `fill_in` works for individual fields but not bulk nested changes
<!-- SECTION:DESCRIPTION:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
Approved implementation plan:
1. Convert index PubSub tests in `collection_live/index_test.exs` and `wishlist_live/index_test.exs` from raw `LiveViewTest.live/2` plus `send(view.pid, :records_index_changed)` to PhoenixTest `visit/2` sessions plus the real `MusicLibrary.Records.broadcast_index_changed/0`. Assert the newly-created record appears on `:index` pages and remains absent while the import modal action ignores the message.
2. Convert remaining raw `live/2` import-flow tests in collection/wishlist to PhoenixTest pipelines (`visit`, `fill_in`, `click_link`, `click_button`, `assert_path`) and use DB assertions for the dynamically-created redirected record.
3. For cart format-change and Release component nested-form interactions, use PhoenixTest as the outer session and keep the smallest `unwrap/2` bridge only for the label-less/nested LiveComponent form change that PhoenixTest cannot express directly.
4. Replace `release_test.exs` `live_isolated/3` coverage with parent LiveView coverage: `CollectionLive.Show` for `show_print?: true` and `ScrobbleLive.ReleaseShow` for `show_print?: false`.
5. Convert `record_actions_test.exs` to PhoenixTest `visit/2` where practical; for the chat-count test, trigger the real Chat component `send_message` flow instead of directly sending `{Chat, :chats_changed}` to the LiveView PID.
6. Run focused tests for the four task files, then `mix format` on changed files. Update the task notes/final summary and check any acceptance-style completion evidence even though this task has no explicit acceptance criteria.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented the approved PhoenixTest migration plan for the four scoped files. Direct LiveView PID messaging was replaced with real PubSub broadcasts or UI/component flows; `live_isolated/3` Release coverage was replaced with routed parent LiveViews; raw `live/2` import flows were converted to PhoenixTest session pipelines. Kept small `unwrap/2` bridges only for interactions PhoenixTest cannot express directly: cart native-select `phx-change`, Release component nested form changes, record action event dispatches, and Chat component form submit.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Converted the ML-182.5 test files away from raw LiveView PID/isolation patterns while preserving the existing behavioral coverage.
Changed:
- `collection_live/index_test.exs`: PubSub tests now use `Records.broadcast_index_changed/0`; import flows use PhoenixTest session pipelines; cart format changes keep a narrow `unwrap/2` helper for the component form change.
- `wishlist_live/index_test.exs`: PubSub and single-item import tests now use PhoenixTest-style navigation and async path assertions.
- `record_actions_test.exs`: page-level tests now mount with `visit/2`; the chat-count test triggers the real Chat component submit flow instead of `send(view.pid, ...)`.
- `components/release_test.exs`: removed the isolated host LiveView and replaced `show_print?` coverage with `CollectionLive.Show` and `ScrobbleLive.ReleaseShow` parent pages.
Validation:
- `mix format test/music_library_web/live/collection_live/index_test.exs test/music_library_web/live/wishlist_live/index_test.exs test/music_library_web/live_helpers/record_actions_test.exs test/music_library_web/components/release_test.exs`
- `mix test test/music_library_web/live/collection_live/index_test.exs test/music_library_web/live/wishlist_live/index_test.exs test/music_library_web/live_helpers/record_actions_test.exs test/music_library_web/components/release_test.exs --max-failures 5` -> 52 passed
- Blocker scan for `live_isolated`, `send(view.pid)`, raw `LiveViewTest.live`, `assert_redirect`, and `LVT` returned no matches in the scoped files.
- `git diff --check` passed.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,65 +0,0 @@
---
id: ML-183
title: Scale Presto full-resolution layout
status: Done
assignee:
- codex
created_date: "2026-05-15 07:01"
updated_date: "2026-05-15 07:19"
labels:
- presto
dependencies: []
modified_files:
- presto/main.py
- presto/poc.py
- presto/README.md
priority: medium
ordinal: 17000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Adjust the Presto MicroPython app after enabling full-resolution 480x480 mode so controls, spacing, and bitmap text render at approximately the same physical size they had in the default doubled-pixel mode.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All hardcoded layout measurements in the Presto app are reviewed and scaled appropriately for full-resolution coordinates.
- [x] #2 Bitmap font scale values are reviewed and adjusted so visible text is not unexpectedly small after full_res=True.
- [x] #3 The app source passes the documented Python syntax check without writing **pycache**.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Scale fixed pixel constants in `presto/main.py` for full-resolution coordinates, using 2x values as the baseline and small fit adjustments where a strict double would push content offscreen.
2. Replace bitmap font scale usages in `presto/main.py` with named full-resolution scale constants so text returns to its prior physical size.
3. Apply the same full-resolution scaling to the small `presto/poc.py` proof-of-concept constants since it was also moved to `full_res=True`.
4. Re-scan for stale unscaled literals that affect layout or font rendering, then run the documented `py_compile` syntax check without creating `__pycache__`.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Scaled main Presto layout constants and bitmap font scale values for full-resolution coordinates. Calendar and keyboard use small fit adjustments from strict 2x values to avoid bottom overflow at 480x480. Row thumbnails now prefer the 150px mini cover source so 80px full-resolution row images do not render from a 40px micro image.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Scaled the Presto full-resolution UI so the move to `Presto(full_res=True)` keeps roughly the same physical sizing as the previous default doubled-pixel mode. `main.py` now uses named scale constants and a `px()` helper for layout measurements, font scales, row heights, scroll indicators, touch drag thresholds, headers, buttons, detail spacing, and keyboard geometry. Calendar cells and keyboard key height use small fit adjustments so their bottom rows stay within 480x480.
`poc.py` was scaled with the same 2x coordinate approach. The README thumbnail note was updated because row thumbnails now prefer `mini_cover_url`, avoiding 40px micro images inside the larger full-resolution row thumbnail box.
Verified with the documented `py_compile` syntax check for `main.py` and `poc.py`; no `__pycache__` output was written.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,370 +0,0 @@
---
id: ML-184
title: test harness for presto application
status: Done
assignee:
- pi
created_date: "2026-05-15 07:18"
updated_date: "2026-05-15 09:48"
labels:
- presto
- ready
dependencies: []
references:
- "https://github.com/iksaif/pimoroni-emu/blob/main/README.md#testing"
- presto/mise.toml
- presto/main.py
- doc-23 - Research-Presto-test-harness-implementation-routes.md
modified_files:
- presto/main.py
- presto/mise.toml
- presto/tests/__init__.py
- presto/tests/conftest.py
- presto/tests/test_screens.py
- presto/README.md
- presto/AGENTS.md
priority: medium
ordinal: 18000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Build a test harness for the Presto application so that it's possible at the very minimum to render screens and check them. The pimoroni-emulator dependency is already configured in presto/mise.toml and provides a DeviceTest testing framework with headless execution, screenshot capture, button simulation, and touch input. Reference: https://github.com/iksaif/pimoroni-emu/blob/main/README.md#testing
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 main.py can be imported without starting the event loop when PRESTO_TEST_MODE=1 is set
- [ ] #2 Deploying to physical Presto still works (mise run presto)
- [x] #3 mise run emulator still opens the emulator window
- [x] #4 All 7 smoke tests pass (home, month, day-empty, day-error, record-detail, search-input, search-results)
- [x] #5 mise run test executes all tests and reports results
- [x] #6 presto/README.md includes a Testing section with usage instructions
- [x] #7 presto/AGENTS.md mentions the test harness
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan: Route A — Minimal guard + emulator smoke tests
### 1. Objective alignment
The goal is a test harness that can, at minimum, render each Presto screen and verify it doesn't crash. Route A achieves this by:
1. Adding a single guard around the `main()` call so the module can be imported without starting the event loop.
2. Writing `DeviceTest`-based tests that import the app module, set up `AppState`, call render functions, and capture screenshots.
3. Configuring a `mise` task (`presto.test`) so tests are runnable with one command.
This directly maps: **problem** = no way to verify screens render correctly without deploying to physical hardware → **solution** = importable module + emulator-based smoke tests.
### 2. Simplicity and alternatives considered
**Chosen: Route A (minimal guard).** The only change to `main.py` is wrapping `main()` in an environment-variable guard. The test harness lives entirely in new files under `presto/tests/`.
**No test `secrets.py` is needed**`secrets` is imported lazily inside `connect_wifi()` (line 516), not at module level. As long as the guard prevents `main()` from running, `connect_wifi()` is never called and the import never happens. The module-level `from presto import Presto` and `Presto(full_res=True)` call are handled by the emulator's mock system.
**Alternatives evaluated and deferred:**
- **Route B (extract lib/ modules):** Cleaner architecture but a 2717-line refactoring with regression risk. Deferred — can be done later as a follow-up, and all tests written for Route A will still pass.
- **Route C (full E2E with touch simulation):** Zero code changes but blocked by WiFi/NTP timeouts (30s each), fragile frame timing, and complex touch sequences. Not viable as a starting point.
**Justification:** Route A is the simplest change that achieves the stated minimum ("render screens and check them") with the least risk. It amortizes into Route B if desired.
**Decision on visual regression:** Smoke tests for this task will verify "renders without exception" only — no `assert_display_matches` with reference fixtures. Screenshots are saved for manual inspection. Reference-based visual regression can be added in a follow-up task once the harness is stable and a baseline is established.
### 3. Completeness and sequencing
#### Step 1: Add test-mode guard to main.py
- Add `import os` at the top of imports (after `import gc` on line 31).
- Replace the bare `main()` call at the bottom (line 2716) with:
```python
if os.environ.get("PRESTO_TEST_MODE") != "1":
main()
```
- Verify the app still deploys and runs on the physical Presto: `mise run presto`.
- Verify the emulator task still works: `cd presto && mise run emulator`.
**Verification:** Deploy to physical device and confirm the app boots normally. Run `mise run emulator` and confirm the emulator window opens with the splash screen. Confirm `PRESTO_TEST_MODE=1` is NOT set in normal `mise run emulator` (it should not be).
#### Step 2: Create test infrastructure
- Create `presto/tests/` directory.
- Create `presto/tests/__init__.py` (empty).
- Create `presto/tests/conftest.py`:
- Set `os.environ["PRESTO_TEST_MODE"] = "1"` before importing `main`.
- `import main` — this imports the app module without starting the event loop.
- Provide a pytest fixture that calls `main.init_display()` once per test class (pen creation depends on `display` which is set up at module level by the emulator mock).
- Provide a helper `make_mock_record()` that returns a dict with all required fields and cache keys pre-set so no draw function attempts a network call (see Step 3 for the exact shape).
- Monkey-patch `main.fetch_thumbnail` to raise `RuntimeError("Network call in smoke test")` so any accidental network access is caught immediately.
- Create `presto/tests/fixtures/` directory for saved screenshots (not reference images — used for manual inspection).
**Verification:** `cd presto && PRESTO_TEST_MODE=1 python3 -c "import main; print('Import OK, AppState:', main.AppState)"` succeeds without starting the event loop and without importing `secrets`.
#### Step 3: Write smoke tests for all 7 screens
Create `presto/tests/test_screens.py` with one test per screen. Every test follows the pattern: `init_display()`, create `AppState()`, set state fields, call draw function, assert no exception, optionally save screenshot.
**Mock record shape** — records passed to draw functions must include ALL of these keys to avoid triggering thumbnail fetches, text re-measurement, or attribute errors:
```python
{
"id": "rec-1",
"selected_release_id": "release-uuid-1",
"title": "Kind of Blue",
"artists": ["Miles Davis"],
"format": "Vinyl",
"release_date": "1959-08-17",
"genres": ["Jazz", "Modal"],
"record_type": "LP",
"purchased_at": "2024-03-15",
# Pre-computed display strings (set by prepare_record_list / _measure_detail_content)
"_display_title": "Kind of Blue",
"_display_artists": "Miles Davis",
"_display_meta": "Vinyl | 1959",
"_display_title_lines": [("Kind of Blue", 120)],
"_display_artist_lines": [("Miles Davis", 120)],
"_display_meta_lines": [("Vinyl | 1959", 120)],
# Thumbnail cache keys — set to None so draw functions use placeholders
"_thumb_url": "",
"_thumb_data": None,
"_thumb_failed": True,
# Detail-view cache keys
"_detail_thumb_data": None,
"_detail_thumb_failed": True,
"_detail_title_lines": [("Kind of Blue", 22)],
"_detail_artist_lines": [("Miles Davis", 15)],
"_detail_genre_lines": [("Jazz, Modal", 15)],
"_detail_meta_lines": [("LP | Vinyl | 1959", 15)],
"_detail_purchased_lines": [("Purchased: 2024-03-15", 15)],
# Row height (computed by prepare_record_list)
"_row_height": 88,
}
```
The conftest.py `make_mock_record()` helper constructs this dict so individual tests don't repeat it.
**Individual tests:**
1. **test_home_screen_renders** — `init_display()`, create `AppState()`, call `draw_home_screen(app)`, assert no exception, take screenshot.
2. **test_month_view_renders** — set `app.screen = STATE_MONTH`, `app.view_year = 2026`, `app.view_month = 5`, call `draw_month_view(app)`, screenshot.
3. **test_day_view_empty_renders** — set `app.screen = STATE_DAY`, `app.day.records = []`, `app.day.error = False`, `app.view_year = 2026`, `app.view_month = 5`, `app.day.selected_day = 15`, call `draw_day_view(app)`, screenshot.
4. **test_day_view_error_renders** — set `app.day.error = True` (records can be anything), call `draw_day_view(app)`, screenshot.
5. **test_record_detail_renders** — set `app.screen = STATE_RECORD`, populate `app.day.records` with one mock record, set `app.detail.selected_index = 0`, `app.detail.source_screen = STATE_DAY` (required by `draw_record_detail`), call `draw_record_detail(app)`, screenshot.
6. **test_search_input_renders** — set `app.screen = STATE_SEARCH_INPUT`, call `draw_search_input(app)`, screenshot.
7. **test_search_results_renders** — set `app.screen = STATE_SEARCH_RESULTS`, populate `app.search.results` with mock records, pre-compute `app.search.content_height` (or call `prepare_record_list` which won't trigger network calls because `_thumb_failed=True` and `_thumb_data=None`), call `draw_search_results(app)`, screenshot.
**Verification:** All 7 tests pass with `pytest presto/tests/ -v`. The `fetch_thumbnail` monkey-patch confirms no network calls were attempted. Manually inspect generated screenshots to confirm they look correct (no blank screens, text is visible, layouts are intact).
#### Step 4: Configure mise task for testing
- Add a `[tasks.test]` section to `presto/mise.toml`:
```toml
[tasks.test]
run = "pytest tests/ -v"
dir = "{{cwd}}"
```
- Test it: `cd presto && mise run test`.
**Verification:** `mise run test` runs all tests and reports results with the standard pytest summary (7 passed).
#### Step 5: Documentation
- Update `presto/README.md` with a "Testing" section explaining:
- Prerequisites: `pimoroni-emulator` (already installed via `mise`), Python 3, `pytest`.
- How to run tests: `mise run test` (in `presto/` directory) or `cd presto && mise run test`.
- What the tests cover: smoke tests for each screen (home, month, day-empty, day-error, record-detail, search-input, search-results).
- How to add new tests: create a test function in `presto/tests/test_screens.py` following the existing pattern (init display, set up state, call draw function, assert no crash).
- That tests use mock data and do not make network requests.
- That screenshots are saved to `presto/tests/fixtures/` for manual inspection.
- Update `presto/AGENTS.md`:
- Add a new section `## Testing` after the "Deployment And Verification" section:
```markdown
## Testing
The test harness lives in `presto/tests/` and uses the `pimoroni-emulator`'s
`DeviceTest` framework for headless smoke testing.
- Run tests: `mise run test`
- Tests import `main.py` without starting the event loop (guarded by `PRESTO_TEST_MODE=1`).
- Smoke tests cover every screen state. When adding a new screen, add a corresponding test in `test_screens.py`.
- All tests use mock data. No network calls are made during test execution.
- Screenshots are saved to `presto/tests/fixtures/` for manual visual inspection.
```
### 4. Verifiability
Each step has explicit verification:
| Step | Verification |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Guard | Deploy to physical Presto, app boots normally. `mise run emulator` opens window. `PRESTO_TEST_MODE` is not set in normal emulator runs. |
| 2. Infrastructure | `PRESTO_TEST_MODE=1 python3 -c "import main; print(main.AppState)"` succeeds without event loop. No `secrets` import occurs. |
| 3. Smoke tests | `pytest presto/tests/ -v` — all 7 tests pass. `fetch_thumbnail` monkey-patch confirms zero network calls. Screenshots visually inspected. |
| 4. Mise task | `cd presto && mise run test` runs all tests and reports 7 passed. |
| 5. Docs | README has "Testing" section. AGENTS.md has "Testing" section with conventions. |
### 5. Architecture impact analysis
| Touchpoint | Impact |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `presto/main.py` | **Minimal:** `import os` added to imports (line 31); `main()` call (line 2716) wrapped in `os.environ` guard. No function signatures, globals, layout constants, or other logic changes. |
| `presto/mise.toml` | **Addition:** new `[tasks.test]` task. Existing `emulator` and `presto` tasks unchanged. |
| `presto/tests/` | **New directory:** `__init__.py`, `conftest.py`, `test_screens.py`, `fixtures/`. No impact on existing code. |
| `presto/README.md` | **Addition:** "Testing" section. |
| `presto/AGENTS.md` | **Addition:** "Testing" section with conventions. |
| Physical Presto device | **No impact.** The guard is a no-op when `PRESTO_TEST_MODE` is unset (normal operation). |
| Emulator | **No impact.** The `emulator` task runs `main.py` normally; the env var is not set. |
**No migration or deprecation needed.** All changes are additive except the `main()` guard.
### 6. Performance profile
- **Test execution time:** Each smoke test renders one screen and optionally saves a PNG screenshot. Screen rendering is fast (tens of ms in the headless emulator) but the dominant cost is `DeviceTest.setUp()` emulator startup (~500ms-1s per test class). All 7 tests in a single class should complete in under 5 seconds. If each test gets its own class (worst case), total time could be ~7-10 seconds.
- **Database queries:** None — tests use mock data and don't hit any API or database.
- **N+1 risks:** None — no database or API calls in smoke tests. The `fetch_thumbnail` monkey-patch guarantees this.
- **Memory footprint:** The emulator's headless display holds a single 480×480 framebuffer (~900KB uncompressed). Negligible for test environments.
- **No latency/throughput concerns** — tests are offline and single-threaded.
### 7. Benchmarking requirements
No ongoing benchmarks are needed for this change. The test harness itself is the verification mechanism.
**One-off validation:** After smoke tests pass, run `time pytest presto/tests/ -v` to record baseline execution time. If future changes cause tests to slow down significantly (>5x), investigate.
### 8. Cost profile
**Zero cost.** All testing uses:
- `pimoroni-emulator` (free, open-source, MIT licensed)
- Local headless rendering (no API calls, no cloud resources)
- Mock data in tests (no network requests)
No paid resources are consumed during test execution.
### 9. Production infrastructure steps
**No production changes required.** The test harness runs entirely locally and does not affect:
- The deployed Phoenix application
- The physical Presto device
- API endpoints or databases
- Coolify, Litestream, or any production service
The only "deployment" consideration is that `main.py` continues to work on the physical Presto after the guard is added — verified in Step 1.
### 10. Documentation updates
| File | Change |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `presto/README.md` | Add "Testing" section: prerequisites, how to run, what's covered, how to add tests, mock data note, screenshot location. |
| `presto/AGENTS.md` | Add `## Testing` section after "Deployment And Verification": how to run tests, architecture (guard + DeviceTest), smoke test conventions, mock data guarantee. |
| `docs/architecture.md` | No changes needed — Presto is not covered in the main architecture doc (it's a separate MicroPython app). |
| `docs/project-conventions.md` | No changes needed — existing testing conventions cover Elixir only. |
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
## Implementation notes
### Step 1: Guard
- Added `import os` after `import gc` in main.py
- Wrapped bare `main()` call with `if os.environ.get("PRESTO_TEST_MODE") != "1":`
- Emulator still launches: `mise run emulator` opens normally
### Step 2: Infrastructure
- Created `presto/tests/__init__.py`, `conftest.py`, `test_screens.py`, `fixtures/`
- conftest sets `PRESTO_TEST_MODE=1`, provides session-scoped `_emulator` fixture
(installs mocks + headless display), `main_module` fixture (imports main lazily),
`init_display` fixture, and `make_mock_record()` helper
- `fetch_thumbnail` is monkey-patched to raise RuntimeError on network access
### Step 3: Smoke tests
- 7 tests pass in 0.58s: home, month, day-empty, day-error, record-detail,
search-input, search-results
- Screenshots saved to `presto/tests/fixtures/`
- No network calls triggered
### Step 4: Mise task
- Added `[tasks.test]` to `presto/mise.toml`: `pytest tests/ -v`
- `mise run test` works from the presto directory
### Step 5: Documentation
- Added "Testing" section to `presto/README.md` with usage, coverage table,
and instructions for adding new tests
- Added "## Testing" section to `presto/AGENTS.md` with conventions
### Key implementation difference from plan
- Did not use `DeviceTest` as base class — instead used pytest session-scoped
fixtures that call `install_mocks()` and `create_display()` directly. This
allows clean pytest integration (fixtures, parametrization) without
unittest.TestCase constraints.
- `import main` happens lazily inside a session-scoped fixture that depends
on the emulator setup fixture, ensuring mocks are in place before the
module-level `Presto(full_res=True)` call executes.
### Acceptance criteria #2 (physical deploy) remains unchecked
Cannot verify without physical Presto device. The guard is a no-op when
`PRESTO_TEST_MODE` is unset, so the change is semantically identical to the
original bare `main()` call.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
## Summary
Added a smoke-test harness for the Presto MicroPython application using the pimoroni-emulator's mock system for headless rendering.
### What changed
- **`presto/main.py`** — Added `import os` and wrapped the bare `main()` call with an environment-variable guard (`if os.environ.get("PRESTO_TEST_MODE") != "1":`). The guard is a no-op in normal operation; the emulator and physical deploy paths are unchanged.
- **`presto/tests/`** — New test directory with:
- `conftest.py` — Session-scoped fixtures that install emulator mocks, create a headless Presto display, import `main` lazily, init pens, and monkey-patch `fetch_thumbnail` to raise on any accidental network call. Provides `make_mock_record()` helper.
- `test_screens.py` — 7 smoke tests (home, month, day-empty, day-error, record-detail, search-input, search-results), all passing in ~0.6s.
- `fixtures/` — 7 screenshots generated per run for manual visual inspection.
- **`presto/mise.toml`** — Added `[tasks.test]` task: `pytest tests/ -v`.
- **`presto/README.md`** — Added "Testing" section with usage instructions and coverage table.
- **`presto/AGENTS.md`** — Added "## Testing" section with conventions.
### Tests
```
7 passed in 0.58s
```
All tests use mock data with `_thumb_failed=True` / `_thumb_data=None` so no network calls occur. The `fetch_thumbnail` monkey-patch acts as a safety net.
### Key implementation note
Used pytest session-scoped fixtures calling `install_mocks()` + `create_display()` directly rather than extending `DeviceTest`. This avoids unittest.TestCase constraints and allows clean pytest fixture injection. `import main` happens lazily inside a fixture that depends on the emulator setup, ensuring mock modules are in `sys.modules` before the module-level `Presto(full_res=True)` call executes.
### Outstanding
Acceptance criterion #2 (physical Presto deployment) could not be verified without the device. The guard change is semantically a no-op when `PRESTO_TEST_MODE` is unset — the code path is identical to the original bare `main()` call.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,83 +0,0 @@
---
id: ML-185
title: Simplify Presto artwork handling
status: Done
assignee: []
created_date: "2026-05-15 21:19"
updated_date: "2026-05-16 20:23"
labels:
- api
- presto
- artwork
dependencies: []
documentation:
- docs/architecture.md
- docs/project-conventions.md
- presto/AGENTS.md
- presto/README.md
modified_files:
- lib/music_library_web/controllers/collection_json.ex
- test/music_library_web/controllers/collection_controller_test.exs
- test/prod.hurl
- presto/main.py
- presto/README.md
- presto/tests/conftest.py
- presto/tests/test_screens.py
priority: medium
ordinal: 19000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Change the collection API artwork contract so the Presto client receives named cover assets instead of choosing among legacy flat cover URL fields or resizing them on the device.
Current state at task creation: `MusicLibraryWeb.CollectionJSON` emitted flat cover URL fields (`cover_url`, `thumb_url`, `mini_cover_url`, `micro_cover_url`). `presto/main.py` laid out list covers at 80 px (`THUMB_SIZE = px(40)` with `UI_SCALE = 2`) and detail covers at 480 px (`DETAIL_COVER_SIZE = px(240)`). The existing asset transform pipeline could already serve original images or resized images by width.
Target state: each API record representation exposes four named cover variants: `original`, `large`, `medium`, and `small`. The Presto app consumes `covers.small` for 80 px record-list thumbnails and `covers.medium` for record-detail artwork, while retaining the intended `DETAIL_COVER_SIZE = px(200)` 400 px detail-cover layout area. The client retains only JPEG decode/draw behavior, placeholder behavior, caching, and the existing rule that network fetches do not happen during drag redraws.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Collection API record JSON exposes four named cover variants: original, large, medium, and small.
- [x] #2 The small cover is 80 px wide for the Presto record-list cover, the Presto detail view uses `covers.medium` within the intended `DETAIL_COVER_SIZE = px(200)` 400 px layout area, the large cover is 1000 px wide, and original is unscaled.
- [x] #3 The Presto application consumes the API-provided small and medium cover URLs directly and no longer contains client-side cover resizing or JPEG scale-selection logic.
- [x] #4 Presto documentation and test fixtures describe the named cover contract and the intended 400 px detail-cover layout, and no longer refer to the legacy cover URL fields.
- [x] #5 Relevant Phoenix and Presto tests are updated and pass.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Update the Phoenix collection JSON contract so records expose named cover variants with widths derived from the API artwork contract and the requested 1000 px large variant.
2. Update the Presto client to read the new cover names, use the 80 px row asset and `covers.medium` detail asset directly, and remove JPEG scale-selection/resizing code while preserving `DETAIL_COVER_SIZE = px(200)`, caching, placeholders, and drag-scroll network avoidance.
3. Update documentation, fixtures, and smoke/contract tests so the API and device assumptions stay aligned.
4. Run the focused controller tests and Presto syntax/smoke tests; note that physical Presto behavior is not verified unless the device deployment task is run on hardware.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Both implementation subtasks are now Done. `ML-185.1` updated the collection API to emit `covers.original`, `covers.large`, `covers.medium`, and `covers.small`. `ML-185.2` updated the Presto app to consume `covers.small` and `covers.medium`, use an 80 px row cover and the intended `DETAIL_COVER_SIZE = px(200)` 400 px detail-cover layout, and remove JPEG resizing/scale-selection logic. Focused Phoenix controller tests and Presto syntax/smoke tests passed. Physical Presto hardware was not tested. This record was corrected after the detail cover layout was confirmed to be 400 px rather than 460 px.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Completed the artwork handling simplification across the collection API and Presto client.
The API now exposes named cover variants: `original`, `large`, `medium`, and `small`. The legacy flat cover fields were removed from the collection JSON contract.
The Presto app now consumes `covers.small` for record rows and `covers.medium` for detail pages, keeps the detail artwork in the intended `DETAIL_COVER_SIZE = px(200)` 400 px layout area, and no longer performs JPEG dimension probing or scale-selection resizing on-device. Documentation, guidance, mock fixtures, and smoke tests were updated for the named cover contract and corrected detail-cover layout.
Tests run across the subtasks: `mix test test/music_library_web/controllers/collection_controller_test.exs`; Presto syntax check; `mise run test` from `presto/`. Physical Presto hardware was not tested.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,75 +0,0 @@
---
id: ML-185.1
title: Expose named cover sizes in collection API
status: Done
assignee:
- Codex
created_date: "2026-05-15 21:20"
updated_date: "2026-05-15 21:44"
labels:
- api
- artwork
dependencies: []
documentation:
- docs/architecture.md
- docs/project-conventions.md
modified_files:
- lib/music_library_web/controllers/collection_json.ex
- test/music_library_web/controllers/collection_controller_test.exs
- test/prod.hurl
parent_task_id: ML-185
priority: medium
ordinal: 20000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Update the collection JSON record representation so API clients receive a single named cover set instead of the current Presto-specific flat URL fields.
Context for implementer: `MusicLibraryWeb.CollectionJSON` currently builds `cover_url`, `thumb_url`, `mini_cover_url`, and `micro_cover_url` from `MusicLibrary.Assets.Transform`. The asset controller already accepts `Transform.new(hash: record.cover_hash, width: width)` for resized variants and `Transform.new(hash: record.cover_hash)` for the original/converted image. The required named variants are: `original` with no width transform, `large` at 1000 px width, `medium` at 460 px width, and `small` at 80 px width.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Collection API responses that render records include a named cover object with original, large, medium, and small URLs.
- [x] #2 The generated URLs encode no width for original, 1000 px for large, 460 px for medium, and 80 px for small.
- [x] #3 Legacy flat cover URL fields are removed from the collection JSON contract unless a compatibility decision is explicitly documented during implementation.
- [x] #4 Controller tests assert the new cover contract for collection record responses.
- [x] #5 Production Hurl smoke coverage captures and requests a cover URL from the new contract.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Confirm every collection API action that renders a record flows through `MusicLibraryWeb.CollectionJSON.record/1`.
2. Replace the flat cover URL fields with a named cover object, using module-level width constants for `large`, `medium`, and `small` so the contract is easy to audit.
3. Update `test/music_library_web/controllers/collection_controller_test.exs` expected JSON to assert the four named variants and their encoded widths.
4. Update `test/prod.hurl` to capture a cover URL from the new cover object.
5. Run `mix test test/music_library_web/controllers/collection_controller_test.exs` and any focused asset-controller test if URL generation or transform behavior changes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented the collection record cover contract as a `covers` object with `original`, `large`, `medium`, and `small` URLs. The old flat fields were removed from `MusicLibraryWeb.CollectionJSON`; exact-map controller expectations verify the legacy fields are absent. Ran `mix test test/music_library_web/controllers/collection_controller_test.exs` successfully: 16 tests passed. Dependency warnings were emitted during compilation, but no warnings from the changed project files.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated the collection API record representation to expose named cover variants through `covers.original`, `covers.large`, `covers.medium`, and `covers.small`. The generated asset transform widths are unscaled for `original`, 1000 px for `large`, 460 px for `medium`, and 80 px for `small`; the legacy flat cover URL fields are no longer emitted.
Updated the controller test expected payloads to assert the new exact response shape and switched the production Hurl smoke capture to `$.covers.original`.
Tests run: `mix test test/music_library_web/controllers/collection_controller_test.exs` (16 passed).
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,83 +0,0 @@
---
id: ML-185.2
title: Use API-sized covers in Presto
status: Done
assignee:
- Codex
created_date: "2026-05-15 21:22"
updated_date: "2026-05-16 20:23"
labels:
- presto
- artwork
dependencies:
- ML-185.1
documentation:
- docs/project-conventions.md
- presto/AGENTS.md
- presto/README.md
modified_files:
- presto/main.py
- presto/README.md
- presto/AGENTS.md
- presto/tests/conftest.py
- presto/tests/test_screens.py
parent_task_id: ML-185
priority: medium
ordinal: 21000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Update the Presto application so it trusts the collection API to provide named cover URLs while keeping the intended Presto detail-cover layout.
Context for implementer: `presto/main.py` currently renders record-list covers at 80 px and record-detail covers at 480 px. It currently chooses among `mini_cover_url`, `micro_cover_url`, and `thumb_url`, then `draw_jpeg/5` selects JPEG scale flags to fit images into a bounding box. After the API task is complete, records should expose named covers where `small` is the 80 px row cover and `medium` is the detail cover source. Preserve the intended `DETAIL_COVER_SIZE = px(200)` 400 px detail-cover layout. Preserve existing cover byte caching, placeholder rendering, and the rule that drag redraws must not fetch images over the network.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Record-list rows fetch and draw the API-provided 80 px small cover variant.
- [x] #2 Record-detail pages fetch `covers.medium` and keep the intended `DETAIL_COVER_SIZE = px(200)` 400 px detail-cover layout area centered within the 480 px display.
- [x] #3 Presto image drawing no longer selects JPEG scale flags or resizes covers to fit a bounding box.
- [x] #4 Missing or failed cover downloads still render placeholders, and drag redraws still avoid network fetches.
- [x] #5 Presto README, Presto guidance, and smoke-test fixtures match the named cover contract and intended 400 px detail-cover layout.
- [x] #6 The Presto syntax check and smoke tests pass, with physical device verification only claimed if run on the device.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add small helper(s) for reading the named cover object from API records, returning `small` for list rows and `medium` for detail pages.
2. Replace legacy `mini_cover_url` / `micro_cover_url` / `thumb_url` selection and preload checks with the new named cover helpers.
3. Simplify JPEG drawing so it decodes the named image at the requested top-left coordinate and falls back to a placeholder on failures; remove `_jpeg_scale_options` and max-width/max-height fitting behavior while preserving `DETAIL_COVER_SIZE = px(200)`.
4. Keep row and detail image cache keys separate and preserve the existing drag behavior that shows placeholders when data is not already cached.
5. Update `presto/README.md`, `presto/AGENTS.md`, and test mock records to describe and exercise the named cover contract and intended 400 px detail-cover layout.
6. Run `python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.pyc', doraise=True)"` from `presto/` and `mise run test` for the Presto smoke suite.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Updated the Presto cover path to consume only the collection API `covers` object: row covers use `covers.small`, detail covers use `covers.medium`, and the detail cover layout uses the intended `DETAIL_COVER_SIZE = px(200)` 400 px area centered at x=40 on the 480 px display. Removed JPEG dimension probing and scale-selection logic so `draw_jpeg` decodes named JPEGs at the requested position and uses the provided dimensions only for placeholder fallback. Preserved row/detail byte cache separation and drag-time placeholder behavior. Ran `python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.pyc', doraise=True)"` and `mise run test` from `presto/`; both passed. Physical Presto hardware was not tested. This record was corrected after the detail cover layout was confirmed to be 400 px rather than 460 px.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated the Presto app to trust the API-provided cover names. Record rows now read `covers.small`, record detail pages read `covers.medium`, and the detail cover layout remains the intended `DETAIL_COVER_SIZE = px(200)` 400 px area centered within the 480 px display.
Removed the JPEG dimension probing and scale-selection code from `draw_jpeg`; it now decodes named JPEGs directly and only uses placeholder dimensions when a fetch or decode fails. Existing row/detail image caches and no-network-during-drag behavior are preserved.
Updated Presto guidance, README API examples, and smoke-test fixtures for the named cover contract, and added a focused smoke test assertion for the expected 80 px row cover and 400 px centered detail-cover layout.
Tests run: Presto syntax check and `mise run test` from `presto/` (8 passed). Physical device behavior was not verified on hardware.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,69 +0,0 @@
---
id: ML-186
title: Improve Presto display redraw performance
status: Done
assignee:
- Codex
created_date: "2026-05-16 20:14"
updated_date: "2026-05-16 20:22"
labels:
- presto
- performance
dependencies: []
references:
- "https://github.com/pimoroni/presto/issues/56"
documentation:
- "https://github.com/pimoroni/presto/blob/main/docs/presto.md"
modified_files:
- presto/main.py
- presto/tests/test_screens.py
priority: medium
ordinal: 22000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Use Pimoroni Presto partial display updates for bounded redraws in the MicroPython app where doing so preserves existing behavior. The current app performs full-screen updates for all draws; the desired outcome is to reduce display transfer work for high-frequency or small-area updates without changing visible UI behavior or making physical-device claims without on-device verification.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Bounded UI redraws use partial display updates when the Presto firmware supports them and safely fall back to full updates otherwise.
- [x] #2 Scrollable fixed-header views continue to render correctly during drag and release redraws.
- [x] #3 Search input and scrobble feedback preserve existing visible behavior while avoiding unnecessary full-screen display transfers where practical.
- [x] #4 Headless smoke tests pass, and any unverified physical-device behavior is clearly not claimed.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add safe display update helpers in `presto/main.py`: a full update wrapper, a partial update wrapper with fallback, and named rectangles for the scroll viewport/search input/scrobble button.
2. Keep full-screen updates for full state transitions and broad redraws, but use partial updates for bounded areas where the buffer content can be refreshed independently.
3. For fixed-header scrollable views, add a bounded redraw mode that clears/redraws only the viewport below the header and calls `partial_update` for that region during drag redraws.
4. For small feedback updates, redraw only the search input field after query text changes and redraw only the scrobble button area when its state changes.
5. Add focused tests around fallback/partial-update behavior if the emulator can observe it, then run the Presto smoke test suite.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented safe partial display update helpers with full-update fallback, viewport-only redraws for scrollable fixed-header views, query-field partial redraws, and scrobble-button partial redraws. After user correction, kept the detail cover layout at `DETAIL_COVER_SIZE = px(200)` and updated tests/docs that incorrectly described the cover as 460px.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented bounded display update support for the Presto MicroPython app. Full-screen transitions still use full updates, while scroll redraws now refresh only the content viewport, search typing refreshes only the query field row, and scrobble loading/done feedback refreshes only the visible button region. Added tests for partial-update dispatch, fallback behavior, scroll viewport updates, search field updates, and scrobble button updates. Kept the detail cover layout at `DETAIL_COVER_SIZE = px(200)` and updated stale tests/docs that incorrectly described the detail cover as 460px.
Verification: `python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.pyc', doraise=True)"`; `python3 -m py_compile tests/test_screens.py`; `mise run test` (14 passed). Physical Presto behavior was not verified on-device.
<!-- SECTION:FINAL_SUMMARY:END -->