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>
121 lines
4.8 KiB
Python
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}
|