""" The show detail modal, measured: the season picker must not move. Reported against a real library. Three faults, one complaint — opening a different season of the same show made the whole modal jump: * a season with a twelve-line synopsis and one with a two-line synopsis put the season picker, and every episode under it, ten lines apart. The synopsis is now a constant three lines for a multi-season show — clamped from above and pinned from below to the same number, so the picker does not move at all. Five was tried first and left a short synopsis sitting over three blank lines; * the cast line was however many lines the cast was long; * the season picker itself was a row of pills with `overflow-x: auto`, so a show with a dozen seasons hid most of them behind a horizontal scrollbar that is close to unusable on a phone. The fixes are geometric, so the assertions are rectangles rather than declarations, in the manner `test_layout_measured.py` established: reading `-webkit-line-clamp: 2` out of the stylesheet says nothing about where the thing below it lands. The markup here is what `video-app.js`'s `OverviewText` and `SeasonMenu` render, class for class — `clamped` always while collapsed, `has-more` only when the text actually overflows (measured in the browser, not counted), and `reserved` only for a multi-season show, which is the only place a season can change underneath the picker. """ import json import shutil import subprocess import textwrap from pathlib import Path import pytest HARNESS = Path(__file__).parent / "harness" / "layout_probe.py" STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" pytestmark = pytest.mark.skipif( shutil.which("google-chrome") is None or not (STATIC / "style.css").exists(), reason="Chrome or the SPA stylesheet is not available") SHORT_SYNOPSIS = "Two teams share a city, and one of them is lying about it." LONG_SYNOPSIS = ( "A courier takes a parcel across a border that closed the week before, " "and finds the town on the other side keeping an arrangement nobody " "there is willing to describe out loud. The season follows the four " "households that made it, the clerk who has been filing the paperwork " "for eleven years without reading it, and the inspector sent to find " "out why a road that leads nowhere is resurfaced every spring. What " "began as an accounting discrepancy turns into a question about who " "the town has been paying, and for what, and whether the answer was " "ever a secret or merely something no one had asked about." ) SHORT_CAST = "A Performer, Another Performer" LONG_CAST = ", ".join(f"Performer Number {n}" for n in range(1, 11)) SEASONS = [(0, "Specials", "4 episodes"), *((n, f"Season {n}", "22 episodes") for n in range(1, 13))] def _detail(block_id, synopsis, cast, *, reserved=True, has_more=True, menu_open=False): """One `.video-detail` as VideoDetailModal renders it for a show.""" overview_cls = " ".join( ["video-detail-overview", "clamped"] + (["has-more"] if has_more else []) + (["reserved"] if reserved else [])) toggle = ('' if has_more else "") options = "".join( f'' for n, name, count in SEASONS) panel = (f'
{options}
' if menu_open else "") # Each block sits in a container of whole-pixel height. Stacked in normal # flow they start at fractional offsets — a modal's own content height is # not an integer number of device pixels — and two identical layouts an # eighth of a pixel apart round to tops one pixel apart, which looks like # a defect in the thing being measured and is not one. return f"""
A Show With Several Seasons

File: shows/S05/e01.mkv · TMDB #0000

{toggle}{synopsis}

★ 8.4 · Mystery, Drama · 1993 · Director: A Creator

{cast}

{panel}
""" FRAGMENT = textwrap.dedent(f"""

One

{SHORT_CAST}

""") + \ _detail("long", LONG_SYNOPSIS, LONG_CAST) + \ _detail("short", SHORT_SYNOPSIS, LONG_CAST, has_more=False) + \ _detail("movie", SHORT_SYNOPSIS, SHORT_CAST, reserved=False, has_more=False) + \ _detail("open", LONG_SYNOPSIS, LONG_CAST, menu_open=True) WIDTHS = [320, 360, 412, 768, 1024] SELECTORS = [ "#oneline .video-detail-overview", "#oneline .video-detail-cast", "#long .video-detail-overview", "#long .video-detail-cast", "#long .video-season-menu", "#long .video-detail-body", "#long .video-overview-toggle", "#short .video-detail-overview", "#short .video-season-menu", "#short .video-detail-body", "#movie .video-detail-overview", "#open .video-season-options", "#open.video-detail", ] @pytest.fixture(scope="module") def measured(tmp_path_factory): """One browser for every width — launching one apiece cost three minutes.""" fragment = tmp_path_factory.mktemp("videodetail") / "fragment.html" fragment.write_text(FRAGMENT) proc = subprocess.run( ["python3", str(HARNESS), ",".join(str(w) for w in WIDTHS), str(fragment), *SELECTORS], capture_output=True, text=True, timeout=180) assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" out = json.loads(proc.stdout) assert "error" not in out, f"no measurement: {out}" return out def _box(measured, width, selector): box = measured[str(width)]["boxes"][selector] assert box is not None, f"{selector} did not render at {width} px" return box def _line(measured, width): """One line of synopsis, at this width.""" return _box(measured, width, "#oneline .video-detail-overview")["height"] @pytest.mark.parametrize("width", WIDTHS) def test_the_season_picker_does_not_move_with_the_synopsis(measured, width): """The complaint itself. The two blocks differ in one thing only, which is the one thing that changes when a season is selected: the synopsis. Everything above the picker is show-level and identical between them. One is a single line, the other twelve — the widest gap a season change can produce — and the answer must be the same pixel, not a narrow band. Measured from each modal's own body, since the blocks are stacked down the page. """ def offset(block): return (_box(measured, width, f"#{block} .video-season-menu")["top"] - _box(measured, width, f"#{block} .video-detail-body")["top"]) long_off, short_off = offset("long"), offset("short") assert long_off == short_off, ( f"at {width} px the season picker sits {long_off} px down for a long " f"synopsis and {short_off} px down for a short one — a " f"{abs(long_off - short_off)} px jump on every season change") @pytest.mark.parametrize("width", WIDTHS) def test_the_synopsis_is_three_lines_whatever_it_says(measured, width): """Clamped from above and pinned from below to the same three lines.""" line = _line(measured, width) for block in ("long", "short"): h = _box(measured, width, f"#{block} .video-detail-overview")["height"] assert abs(h - 3 * line) <= 4, ( f"the {block} synopsis is {h} px at {width} px, and one line is " f"{line} px — that is {h / line:.1f} lines, not three") @pytest.mark.parametrize("width", WIDTHS) def test_a_read_more_link_lands_on_the_last_line(measured, width): """The float trick, as geometry. `-webkit-line-clamp` cannot leave room after its ellipsis, so the toggle is floated into the last line box instead. If the float spacer is dropped the link goes to line one; if `clear: right` is dropped it shares a line with the text somewhere in the middle. """ line = _line(measured, width) top = _box(measured, width, "#long .video-detail-overview")["top"] link = _box(measured, width, "#long .video-overview-toggle") offset = link["top"] - top assert 2 * line - 4 <= offset <= 3 * line, ( f"the read-more link starts {offset} px into a three-line box of " f"{line} px lines at {width} px — it is not on the third line") assert link["offRight"] == 0, ( f"{link['offRight']} px of the read-more link is off a {width} px screen") @pytest.mark.parametrize("width", WIDTHS) def test_a_short_synopsis_with_nothing_to_expand_stays_short(measured, width): """The spacer is scoped to `.has-more` for a reason. `overflow: hidden` makes the paragraph a block formatting context, so it contains the four-line-tall float and grows to it. A movie or a single-season show — nothing below it can change — must keep its natural height, with no floor under it either. """ line = _line(measured, width) h = _box(measured, width, "#movie .video-detail-overview")["height"] assert h <= 2 * line + 4, ( f"a two-line synopsis with no season under it occupies {h} px at " f"{width} px, which is {h / line:.1f} lines of mostly nothing") @pytest.mark.parametrize("width", WIDTHS) def test_the_cast_is_two_lines_however_long_the_cast_is(measured, width): """Ten names and two names must give the modal the same height.""" ref = _box(measured, width, "#oneline .video-detail-cast")["height"] long_cast = _box(measured, width, "#long .video-detail-cast")["height"] assert long_cast <= 2 * ref + 4, ( f"a ten-name cast takes {long_cast} px at {width} px against {ref} px " f"for one line — {long_cast / ref:.1f} lines, and the clamp is two") @pytest.mark.parametrize("width", WIDTHS) def test_the_open_season_menu_stays_inside_the_modal(measured, width): """It replaced a horizontally scrolling pill row; it must not overflow sideways in turn, and thirteen seasons must not run off the bottom.""" panel = _box(measured, width, "#open .video-season-options") modal = _box(measured, width, "#open.video-detail") assert panel["offLeft"] == 0 and panel["offRight"] == 0, ( f"the season menu hangs {panel['offLeft']} px off the left and " f"{panel['offRight']} px off the right of a {width} px screen") assert panel["left"] >= modal["left"] and panel["right"] <= modal["right"], ( f"the season menu ({panel['left']}..{panel['right']}) is wider than " f"the modal it belongs to ({modal['left']}..{modal['right']})") assert panel["height"] <= 320, ( f"thirteen seasons make a {panel['height']} px menu — it is meant to " "scroll inside itself, not to become the page") @pytest.mark.parametrize("width", [320, 360, 412]) def test_the_page_does_not_scroll_sideways(measured, width): """The pill row's own scrollbar is gone; nothing may replace it.""" r = measured[str(width)] assert r["docScrollW"] <= r["viewport"]["w"], ( f"the document scrolls to {r['docScrollW']} px on a {width} px screen")