""" Where the season menu's panel opens, as arithmetic. The panel used to be an absolutely positioned child of the picker, and the detail modal clips — `overflow: hidden`, for its rounded corners. A show with a dozen seasons opens a panel taller than the room left under the picker on anything but a tall window, and the seasons past the cut then sat outside the modal where no scroll could reach them. Measured live at a 740 px viewport: the panel wanted 320 px and had 288 px. So it is `position: fixed` now, and `placeSeasonPanel()` decides where. That makes it the only part of this menu the stylesheet does not determine, and `test_video_detail_measured.py` says so where the rest of the modal is measured. This is the half a browser cannot be pointed at: pure geometry over a rect and a window height, which is exactly what a node test is for. The function is read out of `video-app.js` rather than duplicated — a copy would keep passing after the original changed. """ import json import re import shutil import subprocess from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" APP = STATIC / "video-app.js" pytestmark = pytest.mark.skipif( shutil.which("node") is None or not APP.exists(), reason="node or the SPA sources are not available") # The three constants and the function, taken as written. Anchored on the # `const SEASON_PANEL_MAX` declaration through the end of the function, so a # rename or a move fails loudly here rather than silently testing nothing. BLOCK = re.compile( r"^const SEASON_PANEL_MAX = .*?^function placeSeasonPanel\(el\) \{.*?^\}", re.M | re.S) @pytest.fixture(scope="module") def source(): m = BLOCK.search(APP.read_text()) assert m, ("placeSeasonPanel is no longer where this test reads it from — " "the season menu's placement is untested until this is fixed") return m.group(0) def _place(tmp_path, source, *, top, bottom, left=40, width=300, inner_height=740): """Run the real function against one trigger rectangle.""" script = tmp_path / "case.js" script.write_text(f""" globalThis.window = {{ innerHeight: {inner_height} }}; {source} const el = {{ getBoundingClientRect: () => ({{ top: {top}, bottom: {bottom}, left: {left}, width: {width} }}) }}; console.log(JSON.stringify(placeSeasonPanel(el))); """) proc = subprocess.run(["node", str(script)], capture_output=True, text=True) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def _px(value): assert value.endswith("px"), f"expected a px length, got {value!r}" return float(value[:-2]) def test_it_opens_downwards_when_the_room_is_below(tmp_path, source): """The ordinary case: the picker sits near the top of the modal.""" pos = _place(tmp_path, source, top=300, bottom=337) assert pos["bottom"] == "auto" assert _px(pos["top"]) == 341, "the panel should start just under the trigger" assert _px(pos["maxHeight"]) == 320, ( "740 - 337 - 12 = 391 px of room below, so the panel takes its full " f"320 px and got {pos['maxHeight']}") def test_it_flips_above_the_trigger_when_the_room_is_up_there(tmp_path, source): """A short window, or a picker pushed down the modal. Absolutely positioned this was the case that lost seasons off the bottom of the modal with nothing able to scroll to them. """ pos = _place(tmp_path, source, top=600, bottom=637) assert pos["top"] == "auto" assert _px(pos["bottom"]) == 740 - 600 + 4, ( "the panel should be pinned to just above the trigger") assert _px(pos["maxHeight"]) == 320, ( "588 px of room above against 91 below — it should open upwards at " f"full height, and got {pos['maxHeight']}") def test_it_never_asks_for_more_room_than_it_has(tmp_path, source): """The defect itself: 320 px of seasons into the 288 px that were free. A picker near the top of a short window — downwards is still the better side, but there is nowhere near 320 px of it. """ pos = _place(tmp_path, source, top=100, bottom=137, inner_height=300) assert pos["bottom"] == "auto", "151 px below against 88 above" assert _px(pos["maxHeight"]) == 300 - 137 - 12, ( "the panel must take the room it actually has, and asked for " f"{pos['maxHeight']}") def test_it_picks_the_side_with_the_room_on_it(tmp_path, source): """Not "below unless below is impossible": whichever side is roomier. A picker two thirds of the way down a short window has 188 px under it and 288 px over it. Opening downwards there would fit, and would still be the smaller menu. """ pos = _place(tmp_path, source, top=300, bottom=337, inner_height=537) assert pos["top"] == "auto", "288 px above against 188 below" assert _px(pos["maxHeight"]) == 288 def test_a_cramped_window_still_gets_a_usable_panel(tmp_path, source): """Squeezed from both sides, it keeps a floor and scrolls inside it. Better a panel that overhangs a little and can be scrolled than one collapsed to a couple of rows — this is a phone in landscape. """ pos = _place(tmp_path, source, top=150, bottom=187, inner_height=340) assert _px(pos["maxHeight"]) == 141, ( "153 px below and 138 above, so it opens downwards into the room it " f"has, and got {pos['maxHeight']}") tighter = _place(tmp_path, source, top=90, bottom=127, inner_height=180) assert _px(tighter["maxHeight"]) == 120, ( "41 px below and 78 above is under the floor either way, so the floor " f"applies, and got {tighter['maxHeight']}") def test_it_takes_the_trigger_s_own_left_and_width(tmp_path, source): """Fixed to the viewport, it no longer inherits the picker's box — the two must be lined up by hand or the menu hangs off its own trigger.""" pos = _place(tmp_path, source, top=300, bottom=337, left=64, width=412) assert pos["left"] == "64px" and pos["width"] == "412px" def test_no_element_means_no_position(tmp_path, source): """Called before the picker is mounted, it must not invent coordinates — the panel renders only once there is a `pos` to render it at.""" script = tmp_path / "none.js" script.write_text(f""" globalThis.window = {{ innerHeight: 740 }}; {source} console.log(JSON.stringify(placeSeasonPanel(null))); """) proc = subprocess.run(["node", str(script)], capture_output=True, text=True) assert proc.returncode == 0, proc.stderr assert json.loads(proc.stdout) is None