phase: 04_reachable_share_cta
Build and Push Containers / test (push) Successful in 47s
Build and Push Containers / build-and-push-app (push) Successful in 10s

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:
2026-11-01 23:26:36 -05:00
parent d43eb407f8
commit bf027672b5
34 changed files with 670 additions and 9 deletions
+88
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import re
import time
from typing import ClassVar
import pytest
from playwright.sync_api import expect
@@ -294,6 +295,93 @@ class TestChromeVisibility:
expect(sharer.locator("#start")).not_to_be_visible()
expect(sharer.locator("#placeholder")).not_to_be_visible()
class TestStageSizing:
"""The stage has a floor, a ceiling, and an escape hatch.
``aspect-ratio: 16 / 10`` alone made the stage 180 px tall at 320 px wide
(the idle placeholder needed ~240 px there) and 543 px tall in a 480 px
window (the Share button ended up below the fold). These tests pin the
six shapes measured in ``00_phase.md``: at each one ``#start`` must be
fully inside *both* the stage box and the viewport — inside the stage or
``overflow: hidden`` clips it, and inside the viewport or a finger can
never reach it. The 1080p case pins the ceiling: the Session panel must
be reachable without scrolling past a 964 px preview.
"""
# The three clip cases (button clipped out of the stage) and the two
# below-the-fold cases (stage taller than the viewport), plus 360x640,
# the fourth measured narrow shape.
FAILURE_VIEWPORTS: ClassVar[list[tuple[int, int]]] = [
(320, 568),
(360, 640),
(390, 844),
(480, 800),
(900, 480),
(1280, 450),
]
STAGE_PROBE = """() => {
const s = document.querySelector('#preview-stage').getBoundingClientRect();
const r = document.querySelector('#start').getBoundingClientRect();
return {
stage_h: s.height,
stage_bottom: s.bottom,
start: {top: r.top, bottom: r.bottom, right: r.right},
in_stage: r.bottom <= s.bottom + 1 && r.top >= s.top - 1,
in_viewport: r.top >= 0 && r.bottom <= innerHeight && r.right <= innerWidth,
stage_in_viewport: s.bottom <= innerHeight + 1,
};
}"""
@pytest.mark.parametrize("viewport", FAILURE_VIEWPORTS)
def test_the_share_button_is_inside_the_stage_and_the_viewport(
self, base_url, browser, request, viewport
):
context = browser.new_context(viewport={"width": viewport[0], "height": viewport[1]})
request.addfinalizer(context.close)
page = context.new_page()
page.goto(f"{base_url}/mirror", wait_until="domcontentloaded")
expect(page.locator("#start")).to_be_visible(timeout=10_000)
result = page.evaluate(self.STAGE_PROBE)
message = f"{viewport[0]}x{viewport[1]}: {result}"
# The floor (264px, the measured idle placeholder + slack) and the
# ceiling (min(70svh, 640px)) must both hold: the box is never so
# short that the CTA clips out, and never so tall that it leaves
# the viewport.
assert result["stage_h"] >= 263, f"floor failed: {message}"
assert result["in_stage"], f"#start is clipped by the stage: {message}"
assert result["in_viewport"], f"#start is below the fold: {message}"
# The stage itself must also fit: on the two short-window shapes the
# aspect-ratio box used to run 543/564 px tall in a 480/450 px window,
# leaving only a scroll to reach the bottom of the page.
assert result["stage_in_viewport"], f"the stage itself overflows the viewport: {message}"
def test_the_ceiling_keeps_the_session_panel_reachable_at_1080p(
self, base_url, browser, request
):
"""1920x1080: the aspect ratio alone wanted a 964 px stage, which
pushed the Session panel off-screen. The 640 px ceiling must keep
the panel on screen — reachable without scrolling."""
context = browser.new_context(viewport={"width": 1920, "height": 1080})
request.addfinalizer(context.close)
page = context.new_page()
page.goto(f"{base_url}/mirror", wait_until="domcontentloaded")
expect(page.locator("#start")).to_be_visible(timeout=10_000)
result = page.evaluate(
"""() => {
const s = document.querySelector('#preview-stage').getBoundingClientRect();
const p = document.querySelector('.panel').getBoundingClientRect();
return {stage_h: s.height, stage_w: s.width, panel_bottom: p.bottom};
}"""
)
assert result["stage_h"] <= 641, (
f"the 640px ceiling failed (aspect ratio alone wanted 964px): {result}"
)
assert result["panel_bottom"] <= 1080, (
f"the Session panel is not reachable without scrolling: {result}"
)
def test_exit_only_appears_on_the_tile_that_is_on_stage(self, device):
sharer = device("/mirror")
start_sharing(sharer)