Files
FabledSteward/tests/test_dash_layout.py
T
bvandeusenandClaude Opus 5.5 630dcc867e
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m19s
CI / unit (push) Failing after 1m32s
CI / publish (push) Skipped
test(dashboard): a widget's column and order don't follow its content height (#5713)
Runs the real static/dash-layout.js under node (CI's job container has it;
the checkout action runs on it) over a saved layout with 60 random sets of
content heights at 12-24 columns. Expected to fail against today's pack(),
which places by content height: pushed alone so CI shows the guard can fail
before the fix lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:29:56 -04:00

121 lines
4.8 KiB
Python

"""The dashboard layout (static/dash-layout.js) holds still (#5713): where a
widget sits depends on the saved layout and the column count, never on how
tall each widget's content turned out. Content arrives widget by widget and
changes on every poll, so a placement that followed the heights moved widgets
between columns as the page loaded.
These run the real dash-layout.js under node, which CI's job container has
(its checkout action runs on it). Without node the test skips locally and
fails in CI, so it can't pass by not running."""
from __future__ import annotations
import json
import os
import random
import shutil
import subprocess
from pathlib import Path
import pytest
LAYOUT_JS = Path(__file__).resolve().parents[1] / "steward" / "static" / "dash-layout.js"
NODE = shutil.which("node")
RUNNER = """
const fs = require("fs");
global.window = {};
eval(fs.readFileSync(process.argv[1], "utf8"));
const L = window.StewardDashLayout;
const cases = JSON.parse(fs.readFileSync(0, "utf8"));
process.stdout.write(JSON.stringify(cases.map(c => L.pack(c.items, c.cols))));
"""
def _pack(cases: list[dict]) -> list[dict]:
if not NODE:
if os.environ.get("CI"):
pytest.fail("node is missing from the CI image; the layout test needs it")
pytest.skip("needs node")
out = subprocess.run([NODE, "-e", RUNNER, str(LAYOUT_JS)], input=json.dumps(cases),
capture_output=True, text=True, timeout=60, check=True)
return json.loads(out.stdout)
# A saved 12-column layout of the kind the dashboard holds, in saved rows.
SAVED = [
{"id": "overview", "x": 0, "y": 0, "w": 6, "h": 4},
{"id": "alerts", "x": 6, "y": 0, "w": 6, "h": 3},
{"id": "hosts", "x": 6, "y": 3, "w": 6, "h": 5},
{"id": "monitors", "x": 0, "y": 4, "w": 4, "h": 4},
{"id": "docker", "x": 4, "y": 4, "w": 2, "h": 6},
{"id": "routers", "x": 0, "y": 8, "w": 4, "h": 5},
{"id": "chart", "x": 6, "y": 8, "w": 6, "h": 4},
{"id": "snmp", "x": 4, "y": 10, "w": 2, "h": 3},
{"id": "unifi", "x": 0, "y": 13, "w": 12, "h": 3},
]
SPLIT = 7 # view rows per saved row (ROW_SPLIT)
def _viewed(fits: dict[str, int] | None = None) -> list[dict]:
items = []
for s in SAVED:
full = s["h"] * SPLIT
items.append({"id": s["id"], "x": s["x"], "y": s["y"] * SPLIT, "w": s["w"],
"h": (fits or {}).get(s["id"], full), "full": full})
return items
def _random_fits(rng: random.Random) -> dict[str, int]:
"""Content heights as they might land mid-load: some widgets still at
their saved height (not measured yet), the rest anywhere from one saved
row up to the saved height."""
return {s["id"]: rng.randint(SPLIT, s["h"] * SPLIT) for s in SAVED if rng.random() < 0.8}
def _overlap(a: dict, b: dict) -> bool:
return a["x"] < b["x"] + b["w"] and b["x"] < a["x"] + a["w"]
def _shape(pos: dict) -> tuple:
"""What a reader would notice moving: each widget's column, and which of
two widgets sharing a column is above the other."""
cols = tuple(sorted((k, p["x"], p["w"]) for k, p in pos.items()))
above = tuple(sorted((a, b) for a in pos for b in pos
if a != b and _overlap(pos[a], pos[b]) and pos[a]["y"] < pos[b]["y"]))
return cols, above
@pytest.mark.parametrize("cols", [12, 16, 18, 24])
def test_no_content_height_moves_a_widget_to_another_column_or_past_another(cols):
rng = random.Random(5713 + cols)
cases = [{"items": _viewed(), "cols": cols}] + [
{"items": _viewed(_random_fits(rng)), "cols": cols} for _ in range(60)]
results = _pack(cases)
first = _shape(results[0])
for case, pos in zip(cases[1:], results[1:]):
assert _shape(pos) == first, {i["id"]: i["h"] for i in case["items"]}
@pytest.mark.parametrize("cols", [1, 12, 18, 24])
def test_each_widget_is_its_fitted_height_with_no_overlap_and_no_gap_above(cols):
rng = random.Random(cols)
items = _viewed(_random_fits(rng))
pos = _pack([{"items": items, "cols": cols}])[0]
fit = {i["id"]: i["h"] for i in items}
assert {k: p["h"] for k, p in pos.items()} == fit
ids = list(pos)
for i, a in enumerate(ids):
pa = pos[a]
assert 0 <= pa["x"] and pa["x"] + pa["w"] <= cols
for b in ids[i + 1:]:
pb = pos[b]
assert not (_overlap(pa, pb) and pa["y"] < pb["y"] + pb["h"] and pb["y"] < pa["y"] + pa["h"]), (a, b)
# Nothing could rise: it sits on the top or on a widget in its columns.
assert pa["y"] == 0 or any(_overlap(pa, pos[o]) and pos[o]["y"] + pos[o]["h"] == pa["y"]
for o in ids if o != a), a
def test_twelve_columns_keep_the_saved_columns():
pos = _pack([{"items": _viewed(), "cols": 12}])[0]
assert {k: (p["x"], p["w"]) for k, p in pos.items()} == {s["id"]: (s["x"], s["w"]) for s in SAVED}