""" Every cell in a row ends where its row ends. Reported as "un décalage sur les lignes du tableau listant les membres": the horizontal rules between rows came out staggered rather than straight. The cause was `display: flex` on the actions ``. **A flex table cell stops being a table cell** — it no longer stretches to the height of its row, so its `border-bottom` is drawn wherever its own content happens to end. Measured before the fix, in a row whose other cells were `top 76, height 40`, the actions cell was `top 77, height 30`: its rule nine pixels above the rest. Nothing in the stylesheet says this. `min-height: 30px` was already there, added for a related symptom, and reads as though it settles the question. Only the rectangles show it does not — which is what this file is for. """ import json import subprocess from pathlib import Path import shutil 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") WIDTHS = [420, 900] # The members table as `MembersTab` renders it: the owner's row carries no # button, which is the row that used to break. Two ordinary rows after it, so a # rule between two *equal* rows can be told from a rule against the odd one. TABLE = """
UserRole
grenet Owner
toto Member
alice Member
""" ROWS = (1, 2, 3) SELECTORS = [f"tbody tr:nth-child({r}) td:nth-child({c})" for r in ROWS for c in (1, 2, 3)] @pytest.fixture(scope="module") def measured(tmp_path_factory): fragment = tmp_path_factory.mktemp("table") / "fragment.html" fragment.write_text(TABLE) 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 _cells(measured, width: int, row: int) -> list[dict]: boxes = measured[str(width)]["boxes"] return [boxes[f"tbody tr:nth-child({row}) td:nth-child({c})"] for c in (1, 2, 3)] @pytest.mark.parametrize("width", WIDTHS) @pytest.mark.parametrize("row", ROWS) def test_the_rule_under_a_row_is_one_straight_line(measured, width, row): bottoms = [c["top"] + c["height"] for c in _cells(measured, width, row)] assert max(bottoms) - min(bottoms) <= 1, ( f"row {row} at {width}px ends at {bottoms} — the border under the " f"actions cell is drawn {max(bottoms) - min(bottoms)}px off the others") @pytest.mark.parametrize("width", WIDTHS) @pytest.mark.parametrize("row", ROWS) def test_every_cell_in_a_row_starts_at_the_same_height(measured, width, row): tops = [c["top"] for c in _cells(measured, width, row)] assert max(tops) - min(tops) <= 1, f"row {row} at {width}px starts at {tops}" @pytest.mark.parametrize("width", WIDTHS) def test_the_row_without_a_button_is_as_tall_as_the_others(measured, width): """The owner cannot be removed, so that row has an empty actions cell. It still has to be a row, not a thin one that reads as a rendering fault.""" heights = [_cells(measured, width, r)[0]["height"] for r in ROWS] assert max(heights) - min(heights) <= 1, ( f"row heights at {width}px are {heights}") @pytest.mark.parametrize("width", WIDTHS) def test_the_rows_are_stacked_with_no_gap_or_overlap(measured, width): """A cell that does not fill its row leaves the next one starting early or late; consecutive rows meeting exactly is what says the table is intact.""" for row in ROWS[:-1]: below = _cells(measured, width, row + 1)[0]["top"] for cell in _cells(measured, width, row): end = cell["top"] + cell["height"] assert abs(below - end) <= 1, ( f"at {width}px a cell of row {row} ends at {end} while row " f"{row + 1} starts at {below}")