summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_table_rows_measured.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_table_rows_measured.py')
-rw-r--r--packages/meshbay-hub/tests/test_table_rows_measured.py116
1 files changed, 116 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_table_rows_measured.py b/packages/meshbay-hub/tests/test_table_rows_measured.py
new file mode 100644
index 0000000..60ec878
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_table_rows_measured.py
@@ -0,0 +1,116 @@
+"""
+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 `<td>`. **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 = """
+<div class="main"><div class="settings-section">
+<table class="admin-table">
+ <thead><tr><th>User</th><th>Role</th><th></th></tr></thead>
+ <tbody>
+ <tr><td>grenet</td>
+ <td><span class="badge">Owner</span></td>
+ <td class="admin-actions"></td></tr>
+ <tr><td>toto</td>
+ <td><span class="badge">Member</span></td>
+ <td class="admin-actions"><button class="admin-btn danger">Remove</button></td></tr>
+ <tr><td>alice</td>
+ <td><span class="badge">Member</span></td>
+ <td class="admin-actions"><button class="admin-btn danger">Remove</button></td></tr>
+ </tbody>
+</table>
+</div></div>
+"""
+
+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}")