phase: 143_mobile_keyboard_after_answer
**Phase 143 final verification pass — 100% green, no code changes needed (all work verified in-tree)**
- **Verified implementation:** `COARSE_POINTER` constant (L395, L391-twin `typeof`-guarded shape) + gated finally-block focus `if (!COARSE_POINTER) input.focus({ preventScroll: true })` in `frontend/assets/app.js`; SETTLE lines/anchor untouched; extended unit pin in `test_frontend_scroll.py`; dedicated two-context E2E `tests/e2e/test_mobile_keyboard_focus.py`
- **C1 desktop byte-identical refocus:** ✅ unit pin passes (gated shape keeps `preventScroll`); desktop E2E asserts composer active; `test_loading_feedback.py` 5 passed (phase-06 regression)
- **C2 mobile keyboard stays down:** ✅ `test_mobile_turn_end_keeps_keyboard_down` passes — `pointer: coarse` sanity assert first, composer NOT active after done
- **C3 gates:** ✅ `uv run pytest --cov=app --cov-report=term-missing` → 3359 passed, `app/` TOTAL 99% (>90%); `uv run pytest tests/e2e/test_mobile_keyboard_focus.py -v --no-cov` → 2 passed in isolation; `uv run ruff check .` clean; `uv run pyright` 0 errors
- **C4 no regression:** ✅ full suite green (all frontend source pins: char counter, send flow, never-scroll); `test_stop_generation.py` 3 passed (single settle)
- **Docs:** README.md fully reviewed — all env vars cross-checked against `app/config.py` defaults, routes/endpoints/tools verified in code; AGENTS.md rules + quick reference verified against repo. Consistent — no changes needed
- **No live-infrastructure changes** (dev Postgres container already running)
- **Next pending phase:** `144_modal_back_button`
This commit is contained in:
@@ -214,12 +214,39 @@ def test_turn_end_focus_does_not_scroll() -> None:
|
||||
"""The turn-end focus-back (phase 06's "always focus back") must not
|
||||
move the viewport: focusing the composer while the user is scrolled
|
||||
up would yank them to the bottom at the moment the turn ends.
|
||||
preventScroll keeps the keyboard flow without the scroll."""
|
||||
preventScroll keeps the keyboard flow without the scroll.
|
||||
|
||||
Phase 143 (TODO L5): the focus-back is now GATED on pointer type —
|
||||
`if (!COARSE_POINTER) input.focus({ preventScroll: true })`. On a
|
||||
coarse pointer (phone/tablet) focusing the composer pops the
|
||||
virtual keyboard over the completed answer, so the composer stays
|
||||
unfocused there (the ≥44px tap target starts the next question).
|
||||
This pin's job: the focus-back exists, is preventScroll (the
|
||||
phase-06/42 contract, byte-preserved on desktop), and is
|
||||
coarse-gated — a bare ungated `input.focus(...)` line in the
|
||||
finally block is a failure, as a missing or mistyped
|
||||
`COARSE_POINTER` constant is."""
|
||||
js = _js()
|
||||
# Phase 48: the settle line gained the user-stop terminal (stop →
|
||||
# idle, no banner) — the focus-back contract is unchanged.
|
||||
finally_idx = js.find("// done | error | stop → idle: always settle, always focus back")
|
||||
assert finally_idx != -1, "the turn's finally block must exist"
|
||||
block = js[finally_idx : js.find("\n}", finally_idx)]
|
||||
assert 'input.focus({ preventScroll: true })' in block
|
||||
# Phase 143: the gated shape — the focus call exists, keeps
|
||||
# preventScroll, and is wrapped in the coarse-pointer gate.
|
||||
assert "if (!COARSE_POINTER) input.focus({ preventScroll: true })" in block
|
||||
# A bare, ungated focus line (the pre-phase-143 shape) is gone:
|
||||
# every focus call in the block rides the gate.
|
||||
focus_lines = [ln for ln in block.splitlines() if "input.focus(" in ln]
|
||||
assert focus_lines, "the turn-end focus-back must still exist"
|
||||
assert all("if (!COARSE_POINTER)" in ln for ln in focus_lines), (
|
||||
"the turn-end focus-back must be coarse-pointer gated (phase 143)"
|
||||
)
|
||||
assert "input.focus()" not in block
|
||||
# The L391-twin constant: the same typeof-guarded matchMedia shape
|
||||
# as reducedMotion, bound to COARSE_POINTER.
|
||||
assert re.search(
|
||||
r'const COARSE_POINTER\s*=\s*typeof matchMedia === "function"'
|
||||
r' && matchMedia\("\(pointer: coarse\)"\)\.matches;',
|
||||
js,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user