phase: 04_reachable_share_cta
All verification is complete. The pending working-tree changes are exactly the phase 04 work (CSS, tests, the README narrow-screen line, the AGENTS.md "Two→Three pages" fix) — all accurate. ## Final report **Verified (final pass — all 4 task files present in `complete/`):** - Gate 1: `uv run ruff check . && uv run ruff format --check .` → all checks passed, 29 files formatted - Gate 2: `uv run pyright` → 0 errors - Gate 3: `uv run pytest` → **269 passed, 4 skipped** (realdisplay-gated), coverage 95% - Gate 4: `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → **3 passed, 1 skipped** (host delivers no audio source — documented in module docstring) **Completion criteria:** 1. `#start` elementFromPoint at the 11 required sizes — ✅ `TestIdleShareButtonReachable` sweeps 12 viewports (superset); 12/12 passed 2. `#stop` clickable while sharing — ✅ `TestLiveStopButtonReachable` (320×568, 390×844, 1280×450) passed, incl. placeholder-hidden + livetag assertions 3. Single-cell stage guard — ✅ `test_the_share_stage_is_one_cell` + `test_the_stage_has_a_floor_and_a_ceiling` passed; CSS has `grid-template: 1fr / 1fr`, `.stage > * { grid-area: 1 / 1 }`, `min-height: 264px`, `max-height: min(70svh, 640px)` + vh fallback, `.placeholder { max-height: 100%; overflow: auto }`, `align-content: safe center` 4. Live video fills stage, rounded corners, `#livetag` top-left — ✅ verified by live-browser probe: video fills stage (decoded), stage radius 14px, livetag top-left, placeholder hidden 5. README accurate, AGENTS.md current, suite 100% green — ✅ 6. Phases 01–03 unchanged — ✅ `test_keyboard_wall` + `test_audio_share` + `test_sender_fidelity`: 48 passed **Documentation:** README.md reviewed in full (page table, quickstart, all 12 config defaults vs `config.py`, CLI flags, secure-context/wayland/audio sections, kiosk key table vs `watch.js`/`share.js`, dev commands) — **consistent, no fixes needed**. AGENTS.md verified statement-by-statement (commands, routes, test names, browser/realdisplay flags) — **no stale statements; no changes needed** (the pending "Two→Three pages" fix is correct). **Decisions:** criterion 4 had no dedicated test, so verified directly with a throwaway probe reusing the e2e harness (script in /tmp, not committed). No live infrastructure touched. No defects found in prior work. **Next pending phase:** none — `todo/` is empty of other phases.
This commit is contained in:
+82
-2
@@ -291,8 +291,11 @@ class TestStylesheet:
|
||||
assert re.search(r"\[hidden\]\s*\{[^}]*display:\s*none\s*!important", css), (
|
||||
"app.css must declare a global `[hidden] { display: none !important }`"
|
||||
)
|
||||
# Components that set `display` and are toggled with `hidden` in JS.
|
||||
for selector in (".btn", ".livetag", ".icon-btn", ".toast", ".net"):
|
||||
# Components toggled with `hidden` in JS. All but `.placeholder` set
|
||||
# their own `display`, which would outrank the UA's `[hidden]` without
|
||||
# the global rule; `.placeholder` is guarded because its block is the
|
||||
# escape hatch that keeps its content inside the stage.
|
||||
for selector in (".btn", ".livetag", ".icon-btn", ".toast", ".net", ".placeholder"):
|
||||
assert selector in css, f"{selector} is toggled by `hidden`; it lives in app.css"
|
||||
|
||||
def test_no_rule_reintroduces_a_conflicting_display_for_hidden(self):
|
||||
@@ -337,6 +340,83 @@ class TestStylesheet:
|
||||
"without a pointer — a mouseless kiosk would have no tile controls"
|
||||
)
|
||||
|
||||
def _rules(self) -> list[tuple[str, str]]:
|
||||
r"""app.css as ``(selector, body)`` pairs, comments stripped.
|
||||
|
||||
The flat ``[^{}]+\{[^{}]*\}`` pass treats an ``@media`` header as a
|
||||
selector and the first child's header as its body; the child rules
|
||||
that follow still come back as their own pairs, which is all these
|
||||
guards need — every assertion is about a top-level block (``.stage``,
|
||||
``.stage > *``) or about properties that must sit in the *same* block
|
||||
(``overflow``/``aspect-ratio``/``min-height`` together).
|
||||
"""
|
||||
css = re.sub(r"/\*.*?\*/", "", (STATIC / "app.css").read_text(), flags=re.S)
|
||||
return [(sel.strip(), body) for sel, body in re.findall(r"([^{}]+)\{([^{}]*)\}", css)]
|
||||
|
||||
def test_the_share_stage_is_one_cell(self):
|
||||
"""The stage's children overlap on purpose.
|
||||
|
||||
In plain flow an idle ``<video>`` is intrinsically 300x150, so the
|
||||
placeholder (and the Share button in it) starts 151px below the video
|
||||
at *every* width and runs out of the ``overflow: hidden`` box.
|
||||
``grid-template: 1fr / 1fr`` makes the stage one cell; ``grid-area:
|
||||
1 / 1`` on every child is what puts them all in it. Either half
|
||||
without the other stacks the rows back.
|
||||
"""
|
||||
rules = self._rules()
|
||||
stage_bodies = [body for sel, body in rules if sel == ".stage"]
|
||||
assert stage_bodies, ".stage rule missing from app.css"
|
||||
assert any(
|
||||
re.search(r"grid-template\s*:\s*1fr\s*/\s*1fr", body)
|
||||
or (
|
||||
re.search(r"grid-template-rows\s*:\s*1fr", body)
|
||||
and re.search(r"grid-template-columns\s*:\s*1fr", body)
|
||||
)
|
||||
for body in stage_bodies
|
||||
), (
|
||||
".stage must declare an explicit single cell (grid-template: 1fr / 1fr, "
|
||||
"or grid-template-rows/columns of 1fr) — otherwise its children stack into rows"
|
||||
)
|
||||
child_bodies = [body for sel, body in rules if sel == ".stage > *"]
|
||||
assert child_bodies and any(
|
||||
re.search(r"grid-area\s*:\s*1\s*/\s*1", body) for body in child_bodies
|
||||
), (
|
||||
"app.css must put every .stage child in that one cell (grid-area: 1 / 1) — "
|
||||
"without it the placeholder stacks below the 300x150 video and the Share "
|
||||
"button is clipped by overflow: hidden"
|
||||
)
|
||||
|
||||
def test_the_stage_has_a_floor_and_a_ceiling(self):
|
||||
"""An aspect-derived box with no floor is a clip.
|
||||
|
||||
``aspect-ratio`` alone made the stage 180px tall at 320px wide (the
|
||||
idle placeholder needs 240px there) and 543px tall in a 480px-tall
|
||||
window (the Share button below the fold). The floor and the ceiling
|
||||
make the ratio the *preferred* shape instead of a hard derivation, and
|
||||
the second check is the general statement of the bug: any rule that
|
||||
keeps ``overflow: hidden`` on an aspect-ratio box must carry a
|
||||
``min-height`` in the same block, or the hidden clip silently swallows
|
||||
content again.
|
||||
"""
|
||||
rules = self._rules()
|
||||
stage_bodies = [body for sel, body in rules if sel == ".stage"]
|
||||
assert stage_bodies, ".stage rule missing from app.css"
|
||||
assert any(re.search(r"min-height\s*:", body) for body in stage_bodies), (
|
||||
".stage must keep its floor (min-height)"
|
||||
)
|
||||
assert any(re.search(r"max-height\s*:", body) for body in stage_bodies), (
|
||||
".stage must keep its ceiling (max-height)"
|
||||
)
|
||||
for sel, body in rules:
|
||||
if re.search(r"overflow[^:]*:\s*[^;]*hidden", body) and re.search(
|
||||
r"aspect-ratio\s*:", body
|
||||
):
|
||||
assert re.search(r"min-height\s*:", body), (
|
||||
f"{sel} combines `overflow: hidden` with `aspect-ratio` but has no "
|
||||
"`min-height` in the same block — an aspect-derived box with no "
|
||||
"floor clips its content silently"
|
||||
)
|
||||
|
||||
|
||||
class TestAddressGate:
|
||||
"""mirror has no accounts; the address is the access control."""
|
||||
|
||||
Reference in New Issue
Block a user