ML-184: add Presto smoke test harness
Add a headless smoke-test harness for the Presto microPython app using pimoroni-emulator mock modules. A PRESTO_TEST_MODE=1 guard prevents main() from running at import time so draw functions can be called directly from tests. - presto/main.py: wrap main() with PRESTO_TEST_MODE guard - presto/tests/: conftest with emulator fixtures, 7 smoke tests - presto/mise.toml: add [tasks.test] - presto/README.md, presto/AGENTS.md: add Testing section - .gitignore: exclude test __pycache__, pytest_cache, fixture pngs
This commit is contained in:
@@ -58,3 +58,6 @@ npm-debug.log
|
|||||||
/.superpowers/
|
/.superpowers/
|
||||||
/.pi/extensions/s3-browser/node_modules
|
/.pi/extensions/s3-browser/node_modules
|
||||||
/.pi/extensions/sensitive-file-guard/node_modules
|
/.pi/extensions/sensitive-file-guard/node_modules
|
||||||
|
/presto/tests/__pycache__
|
||||||
|
/presto/tests/fixtures/*.png
|
||||||
|
/presto/tests/.pytest_cache
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
id: ML-184
|
id: ML-184
|
||||||
title: test harness for presto application
|
title: test harness for presto application
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee:
|
||||||
|
- pi
|
||||||
created_date: "2026-05-15 07:18"
|
created_date: "2026-05-15 07:18"
|
||||||
updated_date: "2026-05-15 08:50"
|
updated_date: "2026-05-15 09:48"
|
||||||
labels:
|
labels:
|
||||||
- presto
|
- presto
|
||||||
- ready
|
- ready
|
||||||
@@ -14,6 +15,14 @@ references:
|
|||||||
- presto/mise.toml
|
- presto/mise.toml
|
||||||
- presto/main.py
|
- presto/main.py
|
||||||
- doc-23 - Research-Presto-test-harness-implementation-routes.md
|
- 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
|
priority: medium
|
||||||
ordinal: 18000
|
ordinal: 18000
|
||||||
---
|
---
|
||||||
@@ -30,13 +39,13 @@ Build a test harness for the Presto application so that it's possible at the ver
|
|||||||
|
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
|
|
||||||
- [ ] #1 main.py can be imported without starting the event loop when PRESTO_TEST_MODE=1 is set
|
- [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)
|
- [ ] #2 Deploying to physical Presto still works (mise run presto)
|
||||||
- [ ] #3 mise run emulator still opens the emulator window
|
- [x] #3 mise run emulator still opens the emulator window
|
||||||
- [ ] #4 All 7 smoke tests pass (home, month, day-empty, day-error, record-detail, search-input, search-results)
|
- [x] #4 All 7 smoke tests pass (home, month, day-empty, day-error, record-detail, search-input, search-results)
|
||||||
- [ ] #5 mise run test executes all tests and reports results
|
- [x] #5 mise run test executes all tests and reports results
|
||||||
- [ ] #6 presto/README.md includes a Testing section with usage instructions
|
- [x] #6 presto/README.md includes a Testing section with usage instructions
|
||||||
- [ ] #7 presto/AGENTS.md mentions the test harness
|
- [x] #7 presto/AGENTS.md mentions the test harness
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|
||||||
## Implementation Plan
|
## Implementation Plan
|
||||||
@@ -266,3 +275,96 @@ The only "deployment" consideration is that `main.py` continues to work on the p
|
|||||||
| `docs/project-conventions.md` | No changes needed — existing testing conventions cover Elixir only. |
|
| `docs/project-conventions.md` | No changes needed — existing testing conventions cover Elixir only. |
|
||||||
|
|
||||||
<!-- SECTION:PLAN:END -->
|
<!-- 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 -->
|
||||||
|
|||||||
@@ -31,6 +31,22 @@ python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.py
|
|||||||
|
|
||||||
Do not claim device behavior is verified unless it was tested on the physical Presto.
|
Do not claim device behavior is verified unless it was tested on the physical Presto.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The test harness lives in `presto/tests/` and uses the `pimoroni-emulator`'s
|
||||||
|
mock MicroPython modules 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 `presto/tests/test_screens.py`.
|
||||||
|
- All tests use mock data from `make_mock_record()`. No network calls are
|
||||||
|
made during test execution — `fetch_thumbnail` is monkey-patched to raise
|
||||||
|
on any accidental network access.
|
||||||
|
- Screenshots are saved to `presto/tests/fixtures/` for manual visual
|
||||||
|
inspection.
|
||||||
|
|
||||||
## Hardware And Runtime Constraints
|
## Hardware And Runtime Constraints
|
||||||
|
|
||||||
- Stock Presto firmware: MicroPython, PicoGraphics, touch driver, `urequests`, `ntptime`, usually `jpegdec`.
|
- Stock Presto firmware: MicroPython, PicoGraphics, touch driver, `urequests`, `ntptime`, usually `jpegdec`.
|
||||||
|
|||||||
@@ -130,6 +130,53 @@ presto/
|
|||||||
README.md # This file
|
README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
The test harness uses the [`pimoroni-emulator`](https://github.com/iksaif/pimoroni-emu)
|
||||||
|
(already installed via `mise`) to provide a headless display with mock
|
||||||
|
MicroPython modules. You also need `pytest` (installed via the project venv).
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the presto/ directory:
|
||||||
|
mise run test
|
||||||
|
|
||||||
|
# Or directly:
|
||||||
|
cd presto
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### What the tests cover
|
||||||
|
|
||||||
|
Smoke tests verify that every screen renders without crashing:
|
||||||
|
|
||||||
|
| Test | Screen |
|
||||||
|
| ------------------------ | ---------------------- |
|
||||||
|
| `test_home_screen` | Home / splash |
|
||||||
|
| `test_month_view` | Month calendar grid |
|
||||||
|
| `test_day_view_empty` | Day view (no records) |
|
||||||
|
| `test_day_view_error` | Day view (error state) |
|
||||||
|
| `test_record_detail` | Record detail view |
|
||||||
|
| `test_search_input` | Search input screen |
|
||||||
|
| `test_search_results` | Search results list |
|
||||||
|
|
||||||
|
All tests use mock data and do **not** make network requests.
|
||||||
|
Screenshots are saved to `presto/tests/fixtures/` for manual inspection.
|
||||||
|
|
||||||
|
### Adding new tests
|
||||||
|
|
||||||
|
1. Add a test method to `presto/tests/test_screens.py` following the existing
|
||||||
|
pattern:
|
||||||
|
- Create an `AppState` via `main_module.AppState()`
|
||||||
|
- Set the appropriate view state fields
|
||||||
|
- Call the screen's draw function
|
||||||
|
- Optionally save a screenshot
|
||||||
|
2. Use `make_mock_record()` from `tests.conftest` if your test needs record data.
|
||||||
|
3. Run `mise run test` to verify.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
| Symptom | Likely cause | Fix |
|
| Symptom | Likely cause | Fix |
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Setup:
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import network
|
import network
|
||||||
import ntptime
|
import ntptime
|
||||||
@@ -2713,4 +2714,5 @@ def main():
|
|||||||
# STARTUP
|
# STARTUP
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
if os.environ.get("PRESTO_TEST_MODE") != "1":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Presto test package
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Presto smoke test configuration and helpers.
|
||||||
|
|
||||||
|
Uses the pimoroni-emulator's mock system so ``main.py`` can be imported
|
||||||
|
on CPython without real MicroPython hardware. The emulator's headless
|
||||||
|
display is set up as a session-scoped fixture; ``main`` is imported once
|
||||||
|
after the mocks are in place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Ensure the presto directory is on sys.path so `import main` works.
|
||||||
|
_presto_dir = Path(__file__).resolve().parent.parent
|
||||||
|
if str(_presto_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_presto_dir))
|
||||||
|
|
||||||
|
# Prevent main() from running when we import the module.
|
||||||
|
os.environ["PRESTO_TEST_MODE"] = "1"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Emulator session setup
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def _emulator():
|
||||||
|
"""Install mock MicroPython modules and create a headless Presto display.
|
||||||
|
|
||||||
|
This must run before ``main`` is imported because ``main.py`` imports
|
||||||
|
``presto``, ``network``, ``picographics`` etc. at module level.
|
||||||
|
"""
|
||||||
|
from emulator import _emulator_state
|
||||||
|
from emulator.devices import get_device
|
||||||
|
from emulator.display import create_display
|
||||||
|
from emulator.mocks import install_mocks
|
||||||
|
|
||||||
|
device = get_device("presto")
|
||||||
|
_emulator_state["device"] = device
|
||||||
|
_emulator_state["running"] = True
|
||||||
|
_emulator_state["headless"] = True
|
||||||
|
|
||||||
|
install_mocks()
|
||||||
|
|
||||||
|
display = create_display(device, headless=True)
|
||||||
|
_emulator_state["display"] = display
|
||||||
|
display.init()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
_emulator_state["running"] = False
|
||||||
|
display.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Module fixture — import main once
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def main_module(_emulator):
|
||||||
|
"""Import ``main.py`` after the emulator mocks are installed.
|
||||||
|
|
||||||
|
The ``PRESTO_TEST_MODE=1`` guard ensures ``main()`` does not run.
|
||||||
|
``fetch_thumbnail`` is monkey-patched to raise on any network call.
|
||||||
|
"""
|
||||||
|
import main
|
||||||
|
|
||||||
|
# Guard against accidental network access in smoke tests.
|
||||||
|
def _raise_on_network(*_args, **_kwargs):
|
||||||
|
raise RuntimeError("Network call in smoke test — use mock data")
|
||||||
|
|
||||||
|
main.fetch_thumbnail = _raise_on_network
|
||||||
|
|
||||||
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-class display initialisation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def init_display(main_module):
|
||||||
|
"""Call ``init_display()`` once per test class so pens are ready."""
|
||||||
|
main_module.init_display()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Mock record helper (plain function — no main import needed)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def make_mock_record(index=0):
|
||||||
|
"""Return a fully-populated mock record dict for smoke tests.
|
||||||
|
|
||||||
|
Every key required by draw functions is pre-populated. Thumbnail
|
||||||
|
cache entries are set to ``None`` / ``True`` so the renderer uses
|
||||||
|
grey placeholders and never calls ``fetch_thumbnail``.
|
||||||
|
"""
|
||||||
|
title = f"Mock Album {index}"
|
||||||
|
artist = f"Mock Artist {index}"
|
||||||
|
return {
|
||||||
|
# API-level fields
|
||||||
|
"id": f"rec-{index}",
|
||||||
|
"selected_release_id": f"release-uuid-{index}",
|
||||||
|
"title": title,
|
||||||
|
"artists": [artist],
|
||||||
|
"format": "Vinyl",
|
||||||
|
"release_date": "1959-08-17",
|
||||||
|
"genres": ["Jazz", "Modal"],
|
||||||
|
"record_type": "LP",
|
||||||
|
"purchased_at": "2024-03-15",
|
||||||
|
# Pre-computed display strings (row view)
|
||||||
|
"_display_title": title,
|
||||||
|
"_display_artists": artist,
|
||||||
|
"_display_meta": "Vinyl | 1959",
|
||||||
|
"_display_title_lines": [(title, 120)],
|
||||||
|
"_display_artist_lines": [(artist, 120)],
|
||||||
|
"_display_meta_lines": [("Vinyl | 1959", 120)],
|
||||||
|
# Thumbnail cache (row view) – force placeholder
|
||||||
|
"_thumb_url": "",
|
||||||
|
"_thumb_data": None,
|
||||||
|
"_thumb_failed": True,
|
||||||
|
# Detail-view cache keys
|
||||||
|
"_detail_thumb_data": None,
|
||||||
|
"_detail_thumb_failed": True,
|
||||||
|
"_detail_title_lines": [(title, 22)],
|
||||||
|
"_detail_artist_lines": [(artist, 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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""Smoke tests — verify every Presto screen renders without crashing.
|
||||||
|
|
||||||
|
Each test:
|
||||||
|
1. Creates an ``AppState`` (requires ``init_display`` fixture).
|
||||||
|
2. Sets the appropriate view state fields.
|
||||||
|
3. Calls the screen's draw function.
|
||||||
|
4. Asserts no exception occurred.
|
||||||
|
5. Saves a screenshot to ``presto/tests/fixtures/`` for manual inspection.
|
||||||
|
|
||||||
|
No network calls are made — ``fetch_thumbnail`` is monkey-patched to raise,
|
||||||
|
and mock records have ``_thumb_failed=True`` / ``_thumb_data=None``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.conftest import make_mock_record
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_display")
|
||||||
|
class TestSmokeScreens:
|
||||||
|
"""Smoke tests for all 7 Presto screens."""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_app(main_module):
|
||||||
|
"""Create a fresh ``AppState``."""
|
||||||
|
return main_module.AppState()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _screenshot(main_module, name):
|
||||||
|
"""Save the current emulator framebuffer to fixtures/."""
|
||||||
|
from emulator.testing import screenshot
|
||||||
|
|
||||||
|
path = f"tests/fixtures/{name}.png"
|
||||||
|
screenshot(path)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_home_screen_renders(self, main_module):
|
||||||
|
"""Home / splash screen."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
main_module.draw_home_screen(app)
|
||||||
|
self._screenshot(main_module, "home")
|
||||||
|
|
||||||
|
def test_month_view_renders(self, main_module):
|
||||||
|
"""Month calendar grid."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_MONTH
|
||||||
|
app.view_year = 2026
|
||||||
|
app.view_month = 5
|
||||||
|
main_module.draw_month_view(app)
|
||||||
|
self._screenshot(main_module, "month")
|
||||||
|
|
||||||
|
def test_day_view_empty_renders(self, main_module):
|
||||||
|
"""Day view with no records."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_DAY
|
||||||
|
app.view_year = 2026
|
||||||
|
app.view_month = 5
|
||||||
|
app.day.selected_day = 15
|
||||||
|
app.day.records = []
|
||||||
|
app.day.error = False
|
||||||
|
main_module.draw_day_view(app)
|
||||||
|
self._screenshot(main_module, "day-empty")
|
||||||
|
|
||||||
|
def test_day_view_error_renders(self, main_module):
|
||||||
|
"""Day view with error state."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_DAY
|
||||||
|
app.view_year = 2026
|
||||||
|
app.view_month = 5
|
||||||
|
app.day.selected_day = 15
|
||||||
|
app.day.records = []
|
||||||
|
app.day.error = True
|
||||||
|
main_module.draw_day_view(app)
|
||||||
|
self._screenshot(main_module, "day-error")
|
||||||
|
|
||||||
|
def test_record_detail_renders(self, main_module):
|
||||||
|
"""Record detail view."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_RECORD
|
||||||
|
app.view_year = 2026
|
||||||
|
app.view_month = 5
|
||||||
|
app.day.selected_day = 15
|
||||||
|
|
||||||
|
rec = make_mock_record(0)
|
||||||
|
app.day.records = [rec]
|
||||||
|
app.detail.selected_index = 0
|
||||||
|
app.detail.source_screen = main_module.STATE_DAY
|
||||||
|
|
||||||
|
main_module.draw_record_detail(app)
|
||||||
|
self._screenshot(main_module, "record-detail")
|
||||||
|
|
||||||
|
def test_search_input_renders(self, main_module):
|
||||||
|
"""Search input screen with on-screen keyboard."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_SEARCH_INPUT
|
||||||
|
main_module.draw_search_input(app)
|
||||||
|
self._screenshot(main_module, "search-input")
|
||||||
|
|
||||||
|
def test_search_results_renders(self, main_module):
|
||||||
|
"""Search results list."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_SEARCH_RESULTS
|
||||||
|
|
||||||
|
# Populate results with mock records
|
||||||
|
recs = [make_mock_record(i) for i in range(3)]
|
||||||
|
app.search.results = recs
|
||||||
|
|
||||||
|
# Pre-compute content height (prepare_record_list won't trigger
|
||||||
|
# network calls because _thumb_failed=True and _thumb_url="").
|
||||||
|
app.search.content_height = main_module.prepare_record_list(app, recs)
|
||||||
|
|
||||||
|
main_module.draw_search_results(app)
|
||||||
|
self._screenshot(main_module, "search-results")
|
||||||
Reference in New Issue
Block a user