Skip to content

test(ui): gate table column counts and live-state row shapes - #226

Merged
argszero merged 1 commit into
mainfrom
test/table-column-count-gate
Sep 13, 2026
Merged

argszero merged 1 commit into
mainfrom
test/table-column-count-gate

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

ui/ writes each table's column count by hand in several places — the <thead>, the empty-state emptyRow(N, …) colspan, the load-failure loadErrorRow(N, …) colspan, and the number of <td> in the row template. Nothing fails at runtime when they disagree: a short colspan just paints the row narrow, a long one is silently clipped, and the empty/error row is only visible when the list is empty or the request failed. That is how #97 happened (adding an 8th column to the admin models table left the empty state at colspan=7), and #162 only fixed the symptom by hand.

No issue is closed here: the invariant holds in the tree today, it just has no coverage. src/table_gate.rs is a test-only gate that keeps it holding.

Related Issue

None. Historical incidents this prevents: #97 (found), #162 (fixed by hand).

Changes

  • New src/table_gate.rs — test-only gate, same shape as catalog_gate.rs / i18n_pack.rs: #[cfg(test)] in main.rs, zero new dependencies, no regex, no JS execution, no browser. It reads ui/index.html and ui/js/app.js as text (include_str!) and asserts, for every <tbody> table:
    • positive control: exactly the seven registered tables are found, each with its true <thead> column count — a scanner bug that returns an empty set fails here instead of letting the set assertions pass vacuously;
    • emptyRow(N) colspan == the table's column count;
    • loadErrorRow(N) colspan == the table's column count;
    • the row template has exactly one <td> per column, with whole-row colspan cells (the marketplace detail row) excluded;
    • the only literal colspan is that detail row, and index.html contains none;
    • the transactions table derives its colspan from columns.length rather than hard-coding a number;
    • <tbody> containers receive <tr>-shaped rows (loadErrorRow) and never a bare <div> — the browser hoists a bare <div> out of the table, and the retry button then loses setLiveError's container-level delegation.
  • src/main.rs — four added lines (#[cfg(test)] mod table_gate;, plus a comment). Both files are read at compile time inside a #[cfg(test)] module, so nothing enters the release artifact.
  • No ui/ file is touched, so no cache-bust bump.

Site-to-container attribution is positional (the nearest preceding $("#id").innerHTML = assignment for emptyRow, the setLiveError($("#id"), …) for loadErrorRow) — not a JS parse, and not a new dependency.

Tests

  • Existing tests pass — cargo test: 226 → 235 passed, 0 failed
  • cargo fmt --check passes
  • cargo clippy --all-targets -- -D warnings clean
  • 9 new unit tests added

The assertions are split per dimension so that a single failure names the dimension that drifted (a test asserting several dimensions at once would make per-mutation A/B sets indistinguishable).

A/B (does the gate reject the forms it claims to reject?) — six mutations, each applied alone to ui/ and restored byte-identically (md5 verified before and after), with the unmutated tree as a live control:

leg mutation red set
m1 empty-state colspan 7 → 8 empty_rows_match_their_table_column_count
m2 extra <th> in the models <thead> every_tbody_table_is_scanned_with_its_true_column_count
m3 a whole <td>…</td> removed from the org row template row_templates_have_one_cell_per_column
m4 error-state colspan 6 → 5 error_rows_match_their_table_column_count
m5 dynamic colspan replaced by a literal detail_row_colspan_matches_its_table + the_dynamic_table_derives_its_colspan_from_the_column_list
m6 an unregistered new <table> added every_tbody_table_is_scanned_with_its_true_column_count
ORIG none (restored tree) — (9/9 green)

Five distinct red sets. m2 and m6 are not mutually exclusive (different inputs, same assertion) — recorded as-is rather than presented as six disjoint legs.

Checklist

  • Branch name follows the convention (test/…)
  • Commit message uses Conventional Commits (test(ui): …)
  • Single responsibility, minimal change (2 files, test-only)

ui/ writes each table's column count by hand in several places — the <thead>,
the empty-state emptyRow(N, …) colspan, the load-failure loadErrorRow(N, …)
colspan, and the number of <td> in the row template. Nothing fails at runtime
when they disagree: a short colspan just paints the row narrow, a long one is
silently clipped, and the empty/error row is only visible when the list is
empty or the request failed. That is how #97 happened (adding an 8th column to
the admin models table left the empty state at colspan=7); #162 only fixed the
symptom by hand.

src/table_gate.rs is a test-only gate in the same shape as catalog_gate.rs and
i18n_pack.rs (#[cfg(test)] in main.rs, zero new dependencies, no regex, no JS
execution, no browser). It reads ui/index.html and ui/js/app.js as text and
asserts, for every <tbody> table:

  - positive control: exactly the seven registered tables are found, each with
    its true <thead> column count (a scanner bug that returns an empty set fails
    here instead of letting the set assertions pass vacuously);
  - emptyRow(N) colspan == the table's column count;
  - loadErrorRow(N) colspan == the table's column count;
  - the row template has exactly one <td> per column, with whole-row colspan
    cells (the marketplace detail row) excluded;
  - the only literal colspan is that detail row, and index.html contains none;
  - the transactions table derives its colspan from columns.length instead of
    hard-coding a number;
  - <tbody> containers receive <tr>-shaped rows (loadErrorRow) and never a bare
    <div> — the browser hoists a bare <div> out of the table and the retry
    button then loses setLiveError's container-level delegation.

The assertions are split per dimension so a single failure names the dimension
that drifted. Site-to-container attribution is positional (the nearest preceding
$("#id").innerHTML assignment / setLiveError($("#id"), …)), not a JS parse.

Tests: 9 new; cargo test 226 -> 235, fmt and clippy clean.

A/B: six mutations, each applied alone and restored byte-identically (md5
verified), produce five distinct red sets — empty colspan, thead column count,
row-template cell count, error-row colspan, and the literal/dynamic colspan pair.
An 8th thead column and an unregistered new table both trip the positive control
(same assertion, different inputs — recorded honestly). The unmutated tree is
green on all six legs, as a live control.
@argszero
argszero merged commit 9ecfc67 into main Sep 13, 2026
1 check passed
@argszero
argszero deleted the test/table-column-count-gate branch September 13, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant