RS-21803: Spacer Col test - #67
Conversation
…ol.header.fill CSS Adds coverage for CreateCustomTable's spacer.col/col.header.fill column-header behaviour, including the show.row.headers index shift, the show.col.headers suppression path, and the confirmed out-of-range spacer.col defect where sprintf emits a literal class="NA" header cell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- comment the load-bearing narrowing from bare <th to <th class=" - pin exact colheaderdefault th strings instead of loose substring probes - assert the 6-th-cell recycling consequence of the out-of-range spacer.col - document why spacer.col = 3 (not 2) is required for the label-survival test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds unit coverage for CreateCustomTable’s spacer.col header and CSS behavior.
Changes:
- Tests spacer placement, labels, classes, and row-header offset.
- Covers CSS options and current out-of-range behavior.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Responds to the review comment on PR #67: the only assertions touching the spacer CSS checked that a ".spacer {" selector was present, so removing or altering any of its declarations would still have passed, despite the PR describing this suite as pinning the ".spacer { ... }" block. - Add spacerRule, the complete predefined declaration from createcustomtable.R:868, as one exact string. - Assert it in full in a new test, along with the rule being emitted exactly once and being namespaced under the generated container class rather than defined globally. The container name is randomised per call, so its shape is matched rather than its value; pairing the scoped count with the total count proves the single ".spacer {" occurrence is the scoped one. - Strengthen the use.predefined.css = FALSE flip-check to assert the full rule too, so it cannot be satisfied by a selector whose declarations were emptied. The negative assertion still probes the selector, which is the stronger check for "no rule at all". Verified non-vacuous by mutation: deleting "border: none;" from the production rule fails both new assertions, and passed every assertion before this change. 72 passing, 0 failures, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chschan
left a comment
There was a problem hiding this comment.
Three notes on the new spacer.col coverage.
| expect_false(grepl('class="spacer"', tableHtml(res), fixed = TRUE)) | ||
| }) | ||
|
|
||
| test_that("A single spacer.col index produces exactly one spacer header cell", |
There was a problem hiding this comment.
Coverage gap: spacer.col is never exercised together with show.col.headers = FALSE.
The whole spacer.col application sits inside the if (show.col.headers) branch (R/createcustomtable.R:592-593), so the argument is silently dropped when the header row is off. Verified against master:
h <- CreateCustomTable(m4, show.col.headers = FALSE, spacer.col = 2)$x$text
grepl('class="spacer"', h, fixed = TRUE) # FALSEBoth arguments are covered separately (show.col.headers = FALSE at L246) but never in combination, so this silent-ignore path stays unpinned. One more case would lock in whichever behaviour is intended here.
There was a problem hiding this comment.
Confirmed and covered. Reproduced exactly as you describe — no class="spacer" cell is emitted.
Added "spacer.col is silently ignored when show.col.headers is FALSE", which pins the drop as silent (expect_warning(..., NA)) rather than merely absent, so adding validation later has to be a deliberate decision.
One extra thing the repro turned up, worth having in the same test: the .spacer CSS rule is still emitted, because the predefined CSS block is unconditional. So the stylesheet carries a rule no cell uses. I assert that alongside the negative, which keeps the negative honest — it now fails because no cell was marked, not because the stylesheet went missing. A flip-check with headers on confirms the same call does mark a cell.
Filed as RS-23589 together with the bounds-check defect, since both come down to spacer.col not being validated at its point of use; the ticket asks explicitly whether this path should warn.
| expect_true(grepl('<th class="spacer">Y</th>', h, fixed = TRUE)) | ||
| }) | ||
|
|
||
| test_that("The corner cell shifts the spacer.col index when show.row.headers is TRUE", |
There was a problem hiding this comment.
The comment calls this "the index-shift contract", but the shift isn't a stable +1 — it depends on two unrelated arguments.
corner.styles[1] is prepended once for show.row.headers (R/createcustomtable.R:583) and again for row.spans (:589), so the offset is +0/+1/+2. Verified against master:
CreateCustomTable(m4, show.row.headers = TRUE,
row.spans = list(list(height = 3, label = "G")),
spacer.col = 2)
# <th class="cornerdefault1"></th> | <th class="spacer"></th> | <th class="colheaderdefault1 ">W</th>With row.spans also set, index 2 lands on the second corner cell (empty label) and all four data columns are untouched — the opposite of what this test pins. The assertion is correct for the case it covers; it's the wording that will mislead the next reader. Suggest scoping the comment to show.row.headers alone, or adding the row.spans case alongside it.
There was a problem hiding this comment.
You are right, and the wording was the misleading part — the assertion itself was fine. Verified all three offsets against the same spacer.col = 2:
neither -> <th class="spacer">X</th> (+0, second data column)
show.row.headers -> <th class="spacer">W</th> (+1, first data column)
show.row.headers + row.spans -> <th class="spacer"></th> (+2, second CORNER cell)
Took the second of your two suggestions rather than just narrowing the comment, since the +2 case is the one that actually surprises: index 2 lands on the second corner cell and all four data columns are left untouched, so the spacer marks no data column at all. Narrowing the wording would have documented the trap without pinning it.
Renamed to "Each leading corner cell shifts the spacer.col index by one" and all three offsets are now asserted in one test, with the comment stating that corner.styles[1] is prepended once at :583 and again at :589, so the offset is +0/+1/+2 and not a fixed +1.
Note the fourth combination — row.spans with show.row.headers = FALSE — is not included because it errors on object 'corner.styles' not found. That is RS-23585, pinned separately in #68.
| expect_false(grepl('colheaderdefault', h, fixed = TRUE)) | ||
| }) | ||
|
|
||
| test_that("Out-of-range spacer.col is pinned to its current (defective) behaviour", |
There was a problem hiding this comment.
This pins the benign value and leaves the realistic one uncovered.
spacer.col = 6 survives sprintf() recycling precisely because it's a multiple of the 3-column header vector. The off-by-one someone would actually hit — ncols + 1 — is a hard error. Verified against master:
spacer.col = 4 -> ERROR: arguments cannot be recycled to the same length
spacer.col = 5 -> ERROR: arguments cannot be recycled to the same length
spacer.col = 6 -> OK
Since the fix is going onto the follow-up ticket rather than this PR, pinning the crash is worth as much as pinning the class="NA" output:
expect_error(CreateCustomTable(x2local, show.row.headers = FALSE, spacer.col = 4))There was a problem hiding this comment.
Agreed — pinning the benign value alone undersold the defect. Reproduced your table exactly:
spacer.col = 3 -> OK (3 <th>, 0 NA) <- in range
spacer.col = 4 -> ERROR: arguments cannot be recycled to the same length
spacer.col = 5 -> ERROR: arguments cannot be recycled to the same length
spacer.col = 6 -> OK (6 <th>, 2 NA)
spacer.col = 7 -> ERROR: arguments cannot be recycled to the same length
Added the crash assertions to the same test, anchored on the message so an unrelated future error cannot satisfy them:
for (k in c(4, 5, 7))
expect_error(CreateCustomTable(x2local, show.row.headers = FALSE, spacer.col = k),
"arguments cannot be recycled to the same length", fixed = TRUE, info = k)
expect_error(CreateCustomTable(x2local, show.row.headers = FALSE, spacer.col = 3), NA)I extended your suggestion in two small ways. 5 and 7 are included so the pin is "every out-of-range value that is not a multiple of the header length", rather than one sample that a future partial fix could special-case. And 3 is asserted to succeed, so the errors are attributable to the out-of-range value rather than to spacer.col generally — without it the block would still pass if spacer.col broke outright.
On the follow-up ticket: there was not one. Now filed as RS-23589, covering both failure modes, the k = 3..7 repro, the show.col.headers silent-ignore path from your other comment, and a note that these pinned assertions flip when the bounds check lands. Both test comments reference it.
Responds to review comments on PR #67: - Add coverage for spacer.col combined with show.col.headers = FALSE. The whole spacer.col application sits inside the `if (show.col.headers)` block, so the argument is dropped without warning when the header row is off. Both arguments were covered separately but never together, leaving the silent-ignore path unpinned. The test also asserts the .spacer CSS rule is still emitted (the predefined block is unconditional), which keeps the negative honest: it fails because no cell was marked, not because the stylesheet went missing. - Correct the index-shift test, whose comment described "the index-shift contract" as though it were a fixed +1. corner.styles[1] is prepended once for show.row.headers and again for row.spans, so the offset is +0/+1/+2. All three are now pinned against the same spacer.col = 2. The +2 case is the significant one: index 2 lands on the second corner cell and every data column is left untouched, so the spacer marks no data column at all. - Pin the out-of-range crash alongside the NA-extension case. spacer.col = 6 survives sprintf() recycling only because it is a multiple of the 3-column header vector; the off-by-one a caller would actually make, ncols + 1, is a hard error whose message never mentions spacer.col. 4, 5 and 7 are asserted to error and 3 to succeed, so the errors are attributable to the out-of-range value rather than to spacer.col generally. 88 passing, 0 failures, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RS-23589 covers the missing bounds check on spacer.col (silent class="NA" cells when the index is a multiple of the header length, hard sprintf error otherwise) and the silent-ignore path when show.col.headers is FALSE. Both pinned tests now point at it, so whoever fixes createcustomtable.R has a trail back to why the current behaviour was asserted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Adds unit test coverage for the
spacer.colargument ofCreateCustomTable(R/createcustomtable.R).class="spacer"header cell and the complete.spacer { … }CSS rule, including theuse.predefined.cssvariant..spacerrule is emitted exactly once and is namespaced under the generated container class rather than defined globally. The container name is randomised per call, so its shape is matched rather than its value.spacer.colindexes emission position, not data column, and each leading corner cell shifts it by one.corner.styles[1]is prepended once forshow.row.headersand again forrow.spans, so the offset is +0/+1/+2, not a fixed +1. All three are pinned against the samespacer.col = 2; in the +2 case index 2 lands on the second corner cell and no data column is marked at all.colheaderdefault1, pinned as exact strings.col.header.fillgenuinely displacesbackground: transparent, so the two CSS assertions discriminate rather than coincide.Pinned production defects — RS-23589
spacer.colis never validated at its point of use. Two tests pin the consequences, and both will fail loudly when the bounds check lands.1. No bounds check.
col.header.styles[spacer.col] <- "spacer"silently extends the vector withNA_character_, and the outcome then depends on whether the index happens to be a multiple of the header length:At a multiple (
6) it renders wrongly and silently — literalclass="NA"cells, and 6<th>for a 3-column header because the labels recycle too, re-emittingXandY. Anywhere else it is a hard error fromsprintfwhose message never namesspacer.col. The realistic off-by-one,ncols + 1, is the crash. Both modes are asserted, plus the in-range case, so the errors are attributable to the out-of-range value rather than tospacer.colgenerally.2. Silently ignored when
show.col.headers = FALSE. The wholespacer.colapplication sits inside theif (show.col.headers)block, so with the header row off the argument is dropped with no warning. The.spacerCSS rule is still emitted (the predefined block is unconditional), leaving a rule no cell uses — asserted alongside the negative so it fails because no cell was marked, not because the stylesheet went missing.No production code is changed in this PR.
Jira: https://numbers.atlassian.net/browse/RS-21803
Base
Originally opened stacked on
RS-21803-sigleadercircles(#66). #66 has since merged to master, and this PR is now based onmasterdirectly with no conflicts — the diff is this plan's work only. No merge ordering is required any more.Test plan
88 passing, 0 failures, 0 warnings (24
test_thatblocks; 12 added here).🤖 Generated with Claude Code