lay the invited tab out as the boards' 3x2 grid, tinted blue
Browse filesSix full-width tables stacked vertically did not match the Validation tab and read as six unrelated tables.
Now the boards' own layout and class names: one .lb-track per track, each holding a 2-up .lb-grid of Small | Large, so the six sub-tracks sit 3 down by 2 across in rounded cards with a scroll container. auto-fit with a 460px floor drops to one card per row on a narrow viewport rather than squeezing two unreadable ones.
Table surfaces are tinted light blue (#f2f8ff, headers #e2eefb) so the tab is distinguishable from the two white boards beside it while keeping the same type, borders and spacing.
19 tests, and four negative controls each biting only its own test: flat tables restored, tint reverted to white, content wrapper removed, documentElement restored. One test imported results.py just to compare class names, which pulled in pandas and failed on any interpreter without it; it reads the source as text now and passes under both interpreters.
Renders 3 track sections, 6 cards, 70 rows against the live shortlist, all tags balanced, htranx filtered.
- invited.py +104 -71
- tests/test_invited.py +65 -8
|
@@ -18,51 +18,68 @@ import json
|
|
| 18 |
|
| 19 |
import config
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
# shortlist
|
| 23 |
-
|
| 24 |
-
|
| 25 |
# Deliberately the same variables, fonts and border colours as results._DOC_CSS,
|
| 26 |
# because this tab sits beside the two boards and a different table style reads
|
| 27 |
# as a different system rather than a third view of the same data.
|
| 28 |
CSS = """
|
| 29 |
-
:root{--lb-border:#
|
| 30 |
-
--lb-head:#
|
| 31 |
--lb-mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
|
| 32 |
--lb-sans:'IBM Plex Sans',ui-sans-serif,system-ui,-apple-system,sans-serif}
|
| 33 |
*{box-sizing:border-box}
|
| 34 |
html,body{margin:0;padding:0}
|
| 35 |
body{font-family:var(--lb-sans);color:var(--lb-text);background:transparent;
|
| 36 |
font-size:14px;padding:2px 0 12px}
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
"""
|
| 60 |
|
| 61 |
-
# One colgroup for every table, so a column is in the same place in all six.
|
| 62 |
-
COLS = ("<colgroup><col style='width:2.8rem'><col style='width:20%'>"
|
| 63 |
-
"<col style='width:15%'><col><col style='width:8.5rem'>"
|
| 64 |
-
"<col style='width:7rem'></colgroup>")
|
| 65 |
-
|
| 66 |
# Same self-sizing contract as results.board_document(). Two things it must get
|
| 67 |
# right, both learned there: measure the CONTENT wrapper for height, never
|
| 68 |
# documentElement.scrollHeight, which is bounded below by the iframe viewport and
|
|
@@ -128,6 +145,44 @@ def _head() -> str:
|
|
| 128 |
)
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def document(token: str | None) -> str:
|
| 132 |
"""The whole page, as a standalone HTML document for an iframe srcdoc.
|
| 133 |
|
|
@@ -140,7 +195,7 @@ def document(token: str | None) -> str:
|
|
| 140 |
out = [_head(), "<div id='invroot'>"]
|
| 141 |
|
| 142 |
if not rep or not rep.get("subtracks"):
|
| 143 |
-
out.append("<p class=empty>The invited teams have not been published "
|
| 144 |
"yet. They appear here once the shortlist is final.</p>")
|
| 145 |
out.append(f"</div>{FIT_JS}</body></html>")
|
| 146 |
return "".join(out)
|
|
@@ -154,48 +209,26 @@ def document(token: str | None) -> str:
|
|
| 154 |
n_teams = rep.get("distinct_teams")
|
| 155 |
|
| 156 |
across = f" across <b>{n_teams} teams</b>" if n_teams else ""
|
| 157 |
-
out.append(f"<p class=note><b>{total} seats</b>{across}. "
|
| 158 |
"A team is invited per sub-track, so a team may appear in several. "
|
| 159 |
"If your team is listed, register it in <b>Submit → Test → "
|
| 160 |
"Step 1</b> to collect your registry credentials.</p>")
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
if
|
| 170 |
-
metric += " <span class=selfrep>(self-rep.)</span>"
|
| 171 |
-
out.append(f"<h3>{esc(label)} / {esc(st['division'].capitalize())} "
|
| 172 |
-
f"— {len(rows)} invited</h3>")
|
| 173 |
-
if not rows:
|
| 174 |
-
out.append("<p class=empty>No teams invited in this sub-track.</p>")
|
| 175 |
continue
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
f
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
try:
|
| 183 |
-
score = f"{float(score):.4f}"
|
| 184 |
-
except (TypeError, ValueError):
|
| 185 |
-
score = "—"
|
| 186 |
-
params = t.get("total_params")
|
| 187 |
-
params = (f"{params/1e9:g}B"
|
| 188 |
-
if isinstance(params, (int, float)) and params else "—")
|
| 189 |
-
out.append(
|
| 190 |
-
f"<tr><td class=rank>{i}</td>"
|
| 191 |
-
f"<td class=team>{esc(str(t.get('team_name') or ''))}</td>"
|
| 192 |
-
f"<td class=acct>{esc(str(t.get('hf_user') or ''))}</td>"
|
| 193 |
-
f"<td class=model>{esc(str(t.get('model_name') or ''))}</td>"
|
| 194 |
-
f"<td class=num>{score}</td>"
|
| 195 |
-
f"<td class=num>{esc(params)}</td></tr>")
|
| 196 |
-
out.append("</tbody></table>")
|
| 197 |
-
|
| 198 |
-
out.append("<p class=note>Ranked by each sub-track's own validation metric, "
|
| 199 |
"open-weight entries only, deduplicated by team. Scores are the "
|
| 200 |
"validation results already shown on the Validation board.</p>")
|
| 201 |
out.append(f"</div>{FIT_JS}</body></html>")
|
|
|
|
| 18 |
|
| 19 |
import config
|
| 20 |
|
| 21 |
+
# Order on the page comes from config.TRACKS / config.DIVISIONS at render time,
|
| 22 |
+
# not from the shortlist file, so a regenerated shortlist cannot silently
|
| 23 |
+
# reorder what participants read.
|
| 24 |
+
#
|
| 25 |
# Deliberately the same variables, fonts and border colours as results._DOC_CSS,
|
| 26 |
# because this tab sits beside the two boards and a different table style reads
|
| 27 |
# as a different system rather than a third view of the same data.
|
| 28 |
CSS = """
|
| 29 |
+
:root{--lb-border:#cfe0f2;--lb-text:#1b1830;--lb-sec:#5a6b80;--lb-faint:#8ba0b8;
|
| 30 |
+
--lb-surface:#f2f8ff;--lb-head:#e2eefb;--lb-hover:#e8f2fd;
|
| 31 |
--lb-mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
|
| 32 |
--lb-sans:'IBM Plex Sans',ui-sans-serif,system-ui,-apple-system,sans-serif}
|
| 33 |
*{box-sizing:border-box}
|
| 34 |
html,body{margin:0;padding:0}
|
| 35 |
body{font-family:var(--lb-sans);color:var(--lb-text);background:transparent;
|
| 36 |
font-size:14px;padding:2px 0 12px}
|
| 37 |
+
.lb-note{color:var(--lb-sec);font-size:.87rem;margin:.2rem 0 1.1rem;line-height:1.5}
|
| 38 |
+
.lb-track{margin-top:1.6rem}
|
| 39 |
+
.lb-track:first-of-type{margin-top:.2rem}
|
| 40 |
+
.lb-track-name{font-size:1.2rem;font-weight:700;letter-spacing:-.01em;
|
| 41 |
+
padding-bottom:.45rem;border-bottom:2px solid var(--lb-border);margin-bottom:.8rem}
|
| 42 |
+
/* Same grid as the boards: 3 tracks down, Small|Large across. auto-fit with a
|
| 43 |
+
460px floor means a narrow viewport falls back to one board per row instead
|
| 44 |
+
of squeezing two unreadable ones. */
|
| 45 |
+
.lb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,460px),1fr));gap:.8rem}
|
| 46 |
+
.lb-board{min-width:0;background:var(--lb-surface);border:1px solid var(--lb-border);
|
| 47 |
+
border-radius:12px;overflow:hidden}
|
| 48 |
+
.lb-board-head{padding:.6rem .9rem;border-bottom:1px solid var(--lb-border);
|
| 49 |
+
font-weight:600;font-size:.9rem;background:var(--lb-head)}
|
| 50 |
+
.lb-board-head .n{color:var(--lb-sec);font-weight:400}
|
| 51 |
+
.lb-scroll{overflow:auto;max-width:100%;overscroll-behavior:contain}
|
| 52 |
+
table{width:100%;border-collapse:collapse;table-layout:auto;font-size:.83rem;
|
| 53 |
+
border:0;margin:0;background:var(--lb-surface)}
|
| 54 |
+
th,td{padding-right:1.1rem}
|
| 55 |
+
th:last-child,td:last-child{padding-right:.55rem}
|
| 56 |
+
thead th{font-family:var(--lb-mono);font-size:.66rem;letter-spacing:.04em;
|
| 57 |
+
text-transform:uppercase;color:var(--lb-sec);font-weight:600;text-align:left;
|
| 58 |
+
padding:.45rem .55rem;border-bottom:1px solid var(--lb-border);
|
| 59 |
+
background:var(--lb-head);white-space:nowrap}
|
| 60 |
+
tbody td{padding:.4rem .55rem;border-bottom:1px solid var(--lb-border);
|
| 61 |
+
color:var(--lb-text);white-space:nowrap;vertical-align:middle}
|
| 62 |
+
tbody tr:last-child td{border-bottom:0}
|
| 63 |
+
tbody tr:hover td{background:var(--lb-hover)}
|
| 64 |
+
.lb-rank{font-family:var(--lb-mono);font-weight:600;color:var(--lb-sec)}
|
| 65 |
+
/* Team and model are participant free text (model_name seen at 82 chars), so
|
| 66 |
+
they wrap inside a ch bound. Everything else stays on one line. */
|
| 67 |
+
.lb-team{font-weight:600;white-space:normal;overflow-wrap:anywhere;max-width:14ch}
|
| 68 |
+
.lb-acct{font-family:var(--lb-mono);font-size:.78rem;color:var(--lb-sec);
|
| 69 |
+
white-space:normal;overflow-wrap:anywhere;max-width:12ch}
|
| 70 |
+
.lb-model{color:var(--lb-sec);font-size:.78rem;white-space:normal;
|
| 71 |
+
overflow-wrap:anywhere;max-width:22ch;line-height:1.3}
|
| 72 |
+
.lb-score,.lb-params{font-family:var(--lb-mono);text-align:right}
|
| 73 |
+
.lb-th-num{text-align:right}
|
| 74 |
+
.lb-th-score{background:linear-gradient(rgba(31,111,196,.07),rgba(31,111,196,.07)),var(--lb-head)}
|
| 75 |
+
.lb-score{background:linear-gradient(rgba(31,111,196,.05),rgba(31,111,196,.05))}
|
| 76 |
+
.lb-selfrep{display:inline-block;font-family:var(--lb-sans);font-size:.6rem;
|
| 77 |
+
font-weight:600;text-transform:none;letter-spacing:0;padding:1px 5px;
|
| 78 |
+
border-radius:6px;background:#d6e8fa;color:#3c5a78}
|
| 79 |
+
.lb-empty{padding:1.3rem;color:var(--lb-faint);font-style:italic;text-align:center;
|
| 80 |
+
font-size:.85rem}
|
| 81 |
"""
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# Same self-sizing contract as results.board_document(). Two things it must get
|
| 84 |
# right, both learned there: measure the CONTENT wrapper for height, never
|
| 85 |
# documentElement.scrollHeight, which is bounded below by the iframe viewport and
|
|
|
|
| 145 |
)
|
| 146 |
|
| 147 |
|
| 148 |
+
def _board(st: dict, division: str, esc) -> str:
|
| 149 |
+
"""One division's card: a heading, then that sub-track's ranked table."""
|
| 150 |
+
rows = _rows(st)
|
| 151 |
+
head = (f'<div class="lb-board-head">{esc(division.capitalize())} '
|
| 152 |
+
f'<span class="n">— {len(rows)} invited</span></div>')
|
| 153 |
+
if not rows:
|
| 154 |
+
return (f'<div class="lb-board">{head}'
|
| 155 |
+
f'<div class="lb-empty">No teams invited</div></div>')
|
| 156 |
+
|
| 157 |
+
metric = esc(str(st.get("primary_metric_label") or "Score"))
|
| 158 |
+
if st.get("primary_is_self_reported"):
|
| 159 |
+
metric += ' <span class="lb-selfrep">self-rep.</span>'
|
| 160 |
+
|
| 161 |
+
cells = []
|
| 162 |
+
for i, t in enumerate(rows, 1):
|
| 163 |
+
score = t.get("primary_score")
|
| 164 |
+
try:
|
| 165 |
+
score = f"{float(score):.4f}"
|
| 166 |
+
except (TypeError, ValueError):
|
| 167 |
+
score = "—"
|
| 168 |
+
params = t.get("total_params")
|
| 169 |
+
params = (f"{params / 1e9:g}B"
|
| 170 |
+
if isinstance(params, (int, float)) and params else "—")
|
| 171 |
+
cells.append(
|
| 172 |
+
f'<tr><td class="lb-rank">{i}</td>'
|
| 173 |
+
f'<td class="lb-team">{esc(str(t.get("team_name") or ""))}</td>'
|
| 174 |
+
f'<td class="lb-acct">{esc(str(t.get("hf_user") or ""))}</td>'
|
| 175 |
+
f'<td class="lb-model">{esc(str(t.get("model_name") or ""))}</td>'
|
| 176 |
+
f'<td class="lb-score">{score}</td>'
|
| 177 |
+
f'<td class="lb-params">{esc(params)}</td></tr>')
|
| 178 |
+
|
| 179 |
+
return (f'<div class="lb-board">{head}<div class="lb-scroll"><table>'
|
| 180 |
+
f'<thead><tr><th>#</th><th>Team</th><th>Account</th><th>Model</th>'
|
| 181 |
+
f'<th class="lb-th-num lb-th-score">{metric}</th>'
|
| 182 |
+
f'<th class="lb-th-num">Params</th></tr></thead>'
|
| 183 |
+
f'<tbody>{"".join(cells)}</tbody></table></div></div>')
|
| 184 |
+
|
| 185 |
+
|
| 186 |
def document(token: str | None) -> str:
|
| 187 |
"""The whole page, as a standalone HTML document for an iframe srcdoc.
|
| 188 |
|
|
|
|
| 195 |
out = [_head(), "<div id='invroot'>"]
|
| 196 |
|
| 197 |
if not rep or not rep.get("subtracks"):
|
| 198 |
+
out.append("<p class='lb-empty'>The invited teams have not been published "
|
| 199 |
"yet. They appear here once the shortlist is final.</p>")
|
| 200 |
out.append(f"</div>{FIT_JS}</body></html>")
|
| 201 |
return "".join(out)
|
|
|
|
| 209 |
n_teams = rep.get("distinct_teams")
|
| 210 |
|
| 211 |
across = f" across <b>{n_teams} teams</b>" if n_teams else ""
|
| 212 |
+
out.append(f"<p class='lb-note'><b>{total} seats</b>{across}. "
|
| 213 |
"A team is invited per sub-track, so a team may appear in several. "
|
| 214 |
"If your team is listed, register it in <b>Submit → Test → "
|
| 215 |
"Step 1</b> to collect your registry credentials.</p>")
|
| 216 |
|
| 217 |
+
# One .lb-track per track, each holding a 2-up .lb-grid of Small | Large,
|
| 218 |
+
# so the six sub-tracks lay out 3 down by 2 across. Same structure and class
|
| 219 |
+
# names as results._boards_inner(), so this reads as a third view of the
|
| 220 |
+
# boards rather than a different page.
|
| 221 |
+
for track in config.TRACKS:
|
| 222 |
+
boards = [_board(by_sub[(track, d)], d, esc)
|
| 223 |
+
for d in config.DIVISIONS if (track, d) in by_sub]
|
| 224 |
+
if not boards:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
continue
|
| 226 |
+
label = config.TRACK_LABELS.get(track, track)
|
| 227 |
+
out.append(f'<div class="lb-track"><div class="lb-track-name">'
|
| 228 |
+
f'{esc(str(label))}</div>'
|
| 229 |
+
f'<div class="lb-grid">{"".join(boards)}</div></div>')
|
| 230 |
+
|
| 231 |
+
out.append("<p class='lb-note'>Ranked by each sub-track's own validation metric, "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
"open-weight entries only, deduplicated by team. Scores are the "
|
| 233 |
"validation results already shown on the Validation board.</p>")
|
| 234 |
out.append(f"</div>{FIT_JS}</body></html>")
|
|
@@ -15,6 +15,7 @@ from unittest import mock
|
|
| 15 |
|
| 16 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 17 |
|
|
|
|
| 18 |
import invited # noqa: E402
|
| 19 |
|
| 20 |
|
|
@@ -51,6 +52,25 @@ def _report(n_subtracks=2, rows_per=3, note_rows=0):
|
|
| 51 |
"subtracks": subs}
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def _doc(rep):
|
| 55 |
with mock.patch.object(invited, "load_shortlist_report", lambda t: rep):
|
| 56 |
return invited.document(None)
|
|
@@ -107,20 +127,57 @@ class SelfSizingTest(unittest.TestCase):
|
|
| 107 |
self.assertIn(f"addEventListener('{hook}',fit)", d)
|
| 108 |
|
| 109 |
|
| 110 |
-
class
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
d = _doc(_report())
|
| 118 |
-
self.assertIn("
|
|
|
|
|
|
|
| 119 |
|
| 120 |
def test_long_free_text_wraps_rather_than_widening_the_table(self):
|
| 121 |
"""model_name is participant free text and reaches 80+ chars."""
|
| 122 |
d = _doc(_report())
|
| 123 |
-
self.assertIn("
|
| 124 |
self.assertIn("overflow-wrap:anywhere", d)
|
| 125 |
|
| 126 |
|
|
|
|
| 15 |
|
| 16 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 17 |
|
| 18 |
+
import config # noqa: E402
|
| 19 |
import invited # noqa: E402
|
| 20 |
|
| 21 |
|
|
|
|
| 52 |
"subtracks": subs}
|
| 53 |
|
| 54 |
|
| 55 |
+
def _full_report():
|
| 56 |
+
"""Every real (track, division), so the grid shape can be asserted."""
|
| 57 |
+
subs = []
|
| 58 |
+
for t in config.TRACKS:
|
| 59 |
+
for d in config.DIVISIONS:
|
| 60 |
+
subs.append({
|
| 61 |
+
"track": t, "division": d,
|
| 62 |
+
"track_label": config.TRACK_LABELS.get(t, t),
|
| 63 |
+
"primary_metric_label": "Accuracy",
|
| 64 |
+
"primary_is_self_reported": False,
|
| 65 |
+
"shortlist": [{
|
| 66 |
+
"rank": 1, "team_name": f"{t}-{d}", "hf_user": "u",
|
| 67 |
+
"model_name": "m", "primary_score": 0.5,
|
| 68 |
+
"total_params": 7_000_000_000,
|
| 69 |
+
}],
|
| 70 |
+
})
|
| 71 |
+
return {"distinct_teams": 6, "total_seats": len(subs), "subtracks": subs}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
def _doc(rep):
|
| 75 |
with mock.patch.object(invited, "load_shortlist_report", lambda t: rep):
|
| 76 |
return invited.document(None)
|
|
|
|
| 127 |
self.assertIn(f"addEventListener('{hook}',fit)", d)
|
| 128 |
|
| 129 |
|
| 130 |
+
class GridLayoutTest(unittest.TestCase):
|
| 131 |
+
"""Re-pointed 2026-08-08 from a flat one-table-per-sub-track page.
|
| 132 |
+
|
| 133 |
+
Six full-width tables stacked vertically did not match the Validation tab
|
| 134 |
+
and read as six unrelated tables. The layout is now the boards' own: one
|
| 135 |
+
.lb-track per track, each holding a 2-up .lb-grid of Small | Large, so the
|
| 136 |
+
six sub-tracks sit 3 down by 2 across.
|
| 137 |
+
"""
|
| 138 |
|
| 139 |
+
def test_one_track_section_per_track_not_one_per_sub_track(self):
|
| 140 |
+
d = _doc(_full_report())
|
| 141 |
+
self.assertEqual(len(config.TRACKS), d.count('class="lb-track"'))
|
| 142 |
+
self.assertEqual(len(config.TRACKS), d.count('class="lb-grid"'))
|
| 143 |
+
|
| 144 |
+
def test_each_track_holds_one_board_per_division(self):
|
| 145 |
+
d = _doc(_full_report())
|
| 146 |
+
n = len(config.TRACKS) * len(config.DIVISIONS)
|
| 147 |
+
self.assertEqual(n, d.count('class="lb-board"'))
|
| 148 |
+
self.assertEqual(n, d.count("<table>"))
|
| 149 |
+
|
| 150 |
+
def test_the_grid_is_two_up_and_falls_back_to_one_when_narrow(self):
|
| 151 |
+
d = _doc(_full_report())
|
| 152 |
+
self.assertIn("grid-template-columns:repeat(auto-fit", d)
|
| 153 |
+
self.assertIn("minmax(min(100%,460px),1fr)", d)
|
| 154 |
+
|
| 155 |
+
def test_it_uses_the_same_class_names_as_the_boards(self):
|
| 156 |
+
"""Divergent markup here is how the two views drift apart visually.
|
| 157 |
+
|
| 158 |
+
results.py is read as TEXT rather than imported: importing it pulls in
|
| 159 |
+
pandas, which made this test fail on any interpreter without it, for a
|
| 160 |
+
reason that has nothing to do with the property being checked.
|
| 161 |
+
"""
|
| 162 |
+
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 163 |
+
with open(os.path.join(here, "results.py"), encoding="utf-8") as fh:
|
| 164 |
+
board_src = fh.read()
|
| 165 |
+
for cls in ("lb-track", "lb-track-name", "lb-grid", "lb-board",
|
| 166 |
+
"lb-board-head", "lb-scroll", "lb-rank", "lb-team",
|
| 167 |
+
"lb-model"):
|
| 168 |
+
self.assertIn(cls, board_src, f"{cls} is not a board class")
|
| 169 |
+
self.assertIn(cls, invited.CSS, f"{cls} missing here")
|
| 170 |
+
|
| 171 |
+
def test_the_tables_are_tinted_blue(self):
|
| 172 |
d = _doc(_report())
|
| 173 |
+
self.assertIn("--lb-surface:#f2f8ff", d)
|
| 174 |
+
self.assertIn("--lb-head:#e2eefb", d)
|
| 175 |
+
self.assertIn("background:var(--lb-surface)", d)
|
| 176 |
|
| 177 |
def test_long_free_text_wraps_rather_than_widening_the_table(self):
|
| 178 |
"""model_name is participant free text and reaches 80+ chars."""
|
| 179 |
d = _doc(_report())
|
| 180 |
+
self.assertIn(".lb-team{font-weight:600;white-space:normal", d)
|
| 181 |
self.assertIn("overflow-wrap:anywhere", d)
|
| 182 |
|
| 183 |
|