""" The show detail modal, measured: nothing above the episode list may move. Reported against a real library, three times running, because the jump had more than one cause and fixing one left the others. Opening a different season of the same show moved the season picker and everything under it, so the control just clicked was no longer under the pointer. * The synopsis was however many lines TMDB wrote — two for one season, twelve for the next. * The cast line was however long the cast was. * The episode list is however many episodes the season has, and it lived in the same scroll box as everything else, so a thirteen-episode season made a taller modal than a six-episode one. * `.video-overlay` centres its child, so that taller modal also started *higher* up the screen: the whole dialog rose. * A season that overflowed showed a scrollbar, which narrowed the content by a scrollbar's width, which re-wrapped the file path at the top and shifted everything below it by a line. The last three are the ones a fixed-height synopsis cannot help with, and they are why this file measures the modal's own rectangle and not just its text. 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 season menu's own panel is not here: it is `position: fixed`, placed by `placeSeasonPanel()` rather than by the stylesheet, and `test_season_panel_placement.py` holds that arithmetic to the room it is given. The markup is what `video-app.js` renders, class for class — `clamped` always while collapsed, `has-more` only when the text actually overflows (measured in the browser, not counted), and `reserved`/`video-detail-steady` only for a multi-season show, which is the only place a season can change underneath the picker. """ import json import re 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." ) CAST = ", ".join(f"Performer Number {n}" for n in range(1, 11)) # A path long enough to sit near the wrap boundary, which is what made a # scrollbar's width visible as a one-line shift of everything below it. SOURCE = ("File: shows/A.Show.With.Several.Seasons/S01/" "A.Show.With.Several.Seasons.S01E01.FRENCH.720p.BluRay.x264-grp.mkv") # The modal is `min(calc(100vh - 100px), 760px)` tall and top-anchored 60px # down, so a block of this height holds one whole overlay at the probe's 740px # iframe height with room to spare. BLOCK_H = 800 def _episodes(n): return "".join(f""" """ for i in range(1, n + 1)) def _detail(block_id, synopsis, *, episodes=6, steady=True, has_more=True): """One `.video-detail` inside a real `.video-overlay`, as it is rendered. The overlay matters: it is what centres the modal, and the centring is half of why the dialog rose. `position: absolute` (the rule says `fixed`) keeps `inset: 0` but scopes it to the sized block below, so several of these can be measured in one pass. Each block gets a whole-pixel height. Stacked in normal flow they would start at fractional offsets — a modal's 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 reads as a defect in the thing being measured and is not one. """ overview_cls = " ".join( ["video-detail-overview", "clamped"] + (["has-more"] if has_more else []) + (["reserved"] if steady else [])) toggle = ('' if has_more else "") return f"""
A Show With Several Seasons

{SOURCE} · TMDB #0000

{toggle}{synopsis}

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

{CAST}

{_episodes(episodes)}
""" FRAGMENT = textwrap.dedent("""

One

A Performer, Another Performer

""") + \ _detail("long", LONG_SYNOPSIS) + \ _detail("short", SHORT_SYNOPSIS, has_more=False) + \ _detail("many", SHORT_SYNOPSIS, episodes=24, has_more=False) + \ _detail("movie", SHORT_SYNOPSIS, episodes=1, steady=False, has_more=False) WIDTHS = [320, 360, 412, 768, 1024] # The three blocks whose season picker must land on the same pixel: a long and # a short synopsis, and a short and a long season. STEADY = ["long", "short", "many"] SELECTORS = [ "#oneline .video-detail-overview", "#oneline .video-detail-cast", "#long .video-overview-toggle", "#long .video-detail-cast", "#movie.video-detail", "#movie .video-detail-overview", ] + [ sel.format(b) for b in STEADY for sel in ("#{0}.video-detail", "#{0}-ov.video-overlay", "#{0} .video-detail-body", "#{0} .video-season-menu", "#{0} .video-detail-overview", "#{0} .video-detail-source", "#{0} .video-season-list") ] @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"] def _picker_offset(measured, width, block): """How far the season picker sits inside its own modal body.""" return (_box(measured, width, f"#{block} .video-season-menu")["top"] - _box(measured, width, f"#{block} .video-detail-body")["top"]) @pytest.mark.parametrize("width", WIDTHS) def test_the_season_picker_does_not_move_between_seasons(measured, width): """The complaint itself, over every way one season differs from another. `long` and `short` differ only in the length of the synopsis; `short` and `many` differ only in the number of episodes. Both are things a season change varies, and neither may move the picker by a pixel. """ offsets = {b: _picker_offset(measured, width, b) for b in STEADY} assert len(set(offsets.values())) == 1, ( f"at {width} px the season picker sits at {offsets} px inside its own " "modal body — a jump on every season change") @pytest.mark.parametrize("width", WIDTHS) def test_the_modal_itself_does_not_move_or_resize_between_seasons(measured, width): """The half a fixed-height synopsis cannot reach. `.video-overlay` centres its child, so a season with four times the episodes built a taller modal that started higher up the screen — the title bar and the close button moved too, not just the picker. Top-anchored and one height for every season, the modal is the same rectangle whatever the season holds. """ boxes = {b: _box(measured, width, f"#{b}.video-detail") for b in STEADY} # Against each block's own overlay: the blocks are stacked down the page, # so absolute tops differ by construction and say nothing. tops = {b: box["top"] - _box(measured, width, f"#{b}-ov.video-overlay")["top"] for b, box in boxes.items()} heights = {b: box["height"] for b, box in boxes.items()} assert len(set(tops.values())) == 1, ( f"at {width} px the modal's top edge is at {tops} — the dialog moves " "under the pointer when the season does") assert len(set(heights.values())) == 1, ( f"at {width} px the modal is {heights} px tall — one season's episode " "count is deciding the size of the dialog") @pytest.mark.parametrize("width", WIDTHS) def test_only_the_episode_list_absorbs_the_episode_count(measured, width): """Where the difference is supposed to go instead. A twenty-four-episode season has to overflow something. It must be the list, which scrolls inside a modal of fixed size, and not the modal. """ for block in STEADY: lst = _box(measured, width, f"#{block} .video-season-list") modal = _box(measured, width, f"#{block}.video-detail") assert lst["top"] + lst["height"] <= modal["top"] + modal["height"] + 1, ( f"the {block} episode list runs past the bottom of its own modal " f"at {width} px — it is not the thing scrolling") @pytest.mark.parametrize("width", WIDTHS) def test_the_file_path_wraps_the_same_way_for_every_season(measured, width): """One line of the jump, from a different direction. This is the assertion, but not the whole guard: see `test_the_episode_list_reserves_its_scrollbar` below for why the scrollbar half of it cannot be measured here. """ heights = {b: _box(measured, width, f"#{b} .video-detail-source")["height"] for b in STEADY} assert len(set(heights.values())) == 1, ( f"at {width} px the file path is {heights} px tall depending on the " "season — something above it is changing how it wraps") def test_the_episode_list_reserves_its_scrollbar(): """A declaration, deliberately, and the one place in this file that is. A season that overflows its list draws a scrollbar; one that does not, does not. That is a scrollbar's width of content, which re-wraps the file path above and shifts everything below by a line — the same jump by a different route, and it is visible in the reported screenshots. It cannot be measured here. Headless Chrome gives the probe overlay scrollbars, which take no width, so the geometric test above passes with or without the rule. Reading the declaration is worth less than a rectangle — `test_layout_responsive.py` says so at length — but it is worth more than a test that cannot fail. """ css = (STATIC / "style.css").read_text() rule = re.search( r"\.video-detail\.video-detail-steady \.video-season-list \{([^}]*)\}", css) assert rule, "the steady episode-list rule is gone" assert "scrollbar-gutter: stable" in rule.group(1), ( "without a reserved gutter a scrolling season is a scrollbar narrower " "than a non-scrolling one, and everything above the list re-wraps") @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 STEADY: 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_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_a_movie_is_not_given_a_show_sized_modal(measured, width): """None of this is scoped to every video. A movie and a single-season show have no season to switch to. A fixed height and a floor under the synopsis would buy them nothing but empty space, so neither is applied and the modal is content-sized as before. """ line = _line(measured, width) h = _box(measured, width, "#movie .video-detail-overview")["height"] assert h <= 2 * line + 4, ( f"a one-line synopsis with no season under it occupies {h} px at " f"{width} px, which is {h / line:.1f} lines of mostly nothing") movie = _box(measured, width, "#movie.video-detail")["height"] steady = _box(measured, width, "#short.video-detail")["height"] assert movie < steady, ( f"a one-episode movie modal is {movie} px against a show's {steady} px " "— the show-only fixed height has escaped its scope") @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")