test(ui): gate table column counts and live-state row shapes - #226
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ui/writes each table's column count by hand in several places — the<thead>, the empty-stateemptyRow(N, …)colspan, the load-failureloadErrorRow(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 atcolspan=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.rsis a test-only gate that keeps it holding.Related Issue
None. Historical incidents this prevents: #97 (found), #162 (fixed by hand).
Changes
src/table_gate.rs— test-only gate, same shape ascatalog_gate.rs/i18n_pack.rs:#[cfg(test)]inmain.rs, zero new dependencies, noregex, no JS execution, no browser. It readsui/index.htmlandui/js/app.jsas text (include_str!) and asserts, for every<tbody>table:<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;<td>per column, with whole-row colspan cells (the marketplace detail row) excluded;colspanis that detail row, andindex.htmlcontains none;columns.lengthrather 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 losessetLiveError'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.ui/file is touched, so no cache-bust bump.Site-to-container attribution is positional (the nearest preceding
$("#id").innerHTML =assignment foremptyRow, thesetLiveError($("#id"), …)forloadErrorRow) — not a JS parse, and not a new dependency.Tests
cargo test: 226 → 235 passed, 0 failedcargo fmt --checkpassescargo clippy --all-targets -- -D warningscleanThe 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:empty_rows_match_their_table_column_count<th>in the models<thead>every_tbody_table_is_scanned_with_its_true_column_count<td>…</td>removed from the org row templaterow_templates_have_one_cell_per_columnerror_rows_match_their_table_column_countdetail_row_colspan_matches_its_table+the_dynamic_table_derives_its_colspan_from_the_column_list<table>addedevery_tbody_table_is_scanned_with_its_true_column_countFive 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
test/…)test(ui): …)