Skip to content

RS-21803: Spacer Col test - #67

Merged
SurreyHughesDisplayr merged 5 commits into
masterfrom
RS-21803-spacercol
Aug 25, 2026
Merged

RS-21803: Spacer Col test#67
SurreyHughesDisplayr merged 5 commits into
masterfrom
RS-21803-spacercol

Conversation

@SurreyHughesDisplayr

@SurreyHughesDisplayr SurreyHughesDisplayr commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds unit test coverage for the spacer.col argument of CreateCustomTable (R/createcustomtable.R).

  • Pins the emitted class="spacer" header cell and the complete .spacer { … } CSS rule, including the use.predefined.css variant.
  • Asserts the .spacer rule 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.
  • Asserts the full index-shift behaviour: spacer.col indexes emission position, not data column, and each leading corner cell shifts it by one. corner.styles[1] is prepended once for show.row.headers and again for row.spans, so the offset is +0/+1/+2, not a fixed +1. All three are pinned against the same spacer.col = 2; in the +2 case index 2 lands on the second corner cell and no data column is marked at all.
  • Asserts the spacer cell keeps its own column label rather than being blanked.
  • Asserts the non-spacer header cells keep colheaderdefault1, pinned as exact strings.
  • Asserts col.header.fill genuinely displaces background: transparent, so the two CSS assertions discriminate rather than coincide.

Pinned production defects — RS-23589

spacer.col is 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 with NA_character_, and the outcome then depends on whether the index happens to be a multiple of the header length:

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

At a multiple (6) it renders wrongly and silently — literal class="NA" cells, and 6 <th> for a 3-column header because the labels recycle too, re-emitting X and Y. Anywhere else it is a hard error from sprintf whose message never names spacer.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 to spacer.col generally.

2. Silently ignored when show.col.headers = FALSE. The whole spacer.col application sits inside the if (show.col.headers) block, so with the header row off the argument is dropped with no warning. The .spacer CSS 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 on master directly with no conflicts — the diff is this plan's work only. No merge ordering is required any more.

Test plan

Rscript -e "devtools::load_all('.'); testthat::test_file('tests/testthat/test-createcustomtable.R')"

88 passing, 0 failures, 0 warnings (24 test_that blocks; 12 added here).

🤖 Generated with Claude Code

SurreyHughesDisplayr and others added 2 commits August 20, 2026 17:53
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/testthat/test-createcustomtable.R Outdated
@SurreyHughesDisplayr
SurreyHughesDisplayr changed the base branch from RS-21803-sigleadercircles to master August 25, 2026 00:34
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 chschan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)   # FALSE

Both 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/testthat/test-createcustomtable.R Outdated
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

SurreyHughesDisplayr and others added 2 commits August 25, 2026 13:16
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>

@chschan chschan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@SurreyHughesDisplayr
SurreyHughesDisplayr merged commit bc9fcb4 into master Aug 25, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants