Skip to content

RS-21803: Col Widths and Custom CSS test - #71

Merged
SurreyHughesDisplayr merged 5 commits into
masterfrom
RS-21803-colwidthsandcustomcss
Aug 26, 2026
Merged

RS-21803: Col Widths and Custom CSS test#71
SurreyHughesDisplayr merged 5 commits into
masterfrom
RS-21803-colwidthsandcustomcss

Conversation

@SurreyHughesDisplayr

@SurreyHughesDisplayr SurreyHughesDisplayr commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds unit test coverage for col.widths and custom CSS in CreateCustomTable (R/createcustomtable.R) — 13 test blocks.

  • Pins the emitted <col> tags exactly, including the literal surrounding spaces that paste(..., sep = " ") produces: <col width=' 200px '>. Percentage widths are pinned as an explicit vector so the assertion fails if col.widths is ignored and the default 25% is emitted instead.
  • Covers col.widths counting the row-header column, fewer widths than columns, and more widths than columns (7 widths against a 5-column render, asserting the surplus is emitted untruncated).
  • col.widths.fill.container on and off, with the extracted <table> tag length-guarded so an empty match cannot pass vacuously.
  • Asserts the calc() table-width offset tracks cell.border.width, pinned at a non-default c(5, 2): the width side takes max() = 5 and the height side rev()[1] = 2, so each assertion fails if the two expressions are swapped. A scalar could not tell them apart.
  • Covers the scroll half of the wrapper switch: a row.height (y scroll) or enable.x.scroll switches the widget from boxIframeless to a plain Box, and the x case also pins the emitted div#outer-table-container overflow rule. The custom.css half is already covered by the pre-existing iframes test.
  • Covers override.borders, including the case that pins its substring match as genuinely unanchored — an unrelated .x:nth-child(2) rule alongside border-top suppresses the border. This is the argument's intended opt-out escape hatch, not a defect.

Every argument under test uses a distinctive non-default value, so no assertion can pass while the argument is ignored.

Jira: https://numbers.atlassian.net/browse/RS-21803

Stacked PR

Was stacked on RS-21803-headervisibilityandcellformatting (#70), which has since merged, so the base is now master. Earlier plans in the stack (#66, #67, #68, #69, #70) are all merged; the diff here is this plan's work only.

Test plan

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

252 passing, 0 failures, 0 warnings.

🤖 Generated with Claude Code

SurreyHughesDisplayr and others added 2 commits August 21, 2026 11:57
Add 15 test_that blocks covering col.widths tag emission (single,
comma-separated, vector, NULL, rownames-dependent default, mismatched
lengths), col.widths.fill.container's calc() table width, custom.css
reaching the emitted HTML, the override.borders substring heuristic
across celldefault/colheaderdefault/rowheaderdefault, and the
boxIframeless vs Box widget-host selection driven by custom.css and
scrolling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Genuinely exercise surplus col.widths, pin expected tag vectors instead
of vacuous equality checks, add a case that pins the override.borders
unanchored-substring heuristic, remove a redundant default-valued block,
use a distinct row.header.border.color, add missing expect_length
guards, and fix a stale column-count comment.

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 targeted CreateCustomTable tests for column widths, custom CSS, border overrides, and widget hosting behavior.

Changes:

  • Tests column-width parsing, defaults, overflow, and container sizing.
  • Tests custom CSS emission and border overrides.
  • Tests iframe-less versus scrolling widget hosts.

💡 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
Copilot review (PR #71): normWs() collapses every whitespace run, so
asserting `<col width=' 200px '>` against the normalised HTML would still
pass if the emitted spaces became tabs or repeated spaces. The stated goal
of these blocks is to pin the literal spaces that
paste("<col width='", w, "'>\n") produces, so match the raw HTML instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SurreyHughesDisplayr
SurreyHughesDisplayr changed the base branch from RS-21803-headervisibilityandcellformatting to master August 26, 2026 04:20
@SurreyHughesDisplayr
SurreyHughesDisplayr requested a balanced review from Copilot August 26, 2026 04:21

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

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

tests/testthat/test-createcustomtable.R:1066

  • The test claims that both tokens are required, but its only false case contains border without nth-child. If the implementation dropped the border predicate and keyed solely on nth-child, every assertion here would still pass. Add the reciprocal nth-child-only control.
    cssBorderOnly <- "table.mycustom { border-top: 4px solid rgb(9,9,9); }"

tests/testthat/test-createcustomtable.R:992

  • This negative assertion only excludes the exact 5px value, so the test still passes if disabling col.widths.fill.container incorrectly emits another width:calc(...) value. Assert that the table tag contains no calculated width at all to pin the behavior named by the test.
    expect_false(grepl("width:calc(100% - 5px)", table, fixed = TRUE))

- col.widths.fill.container = FALSE: assert no width:calc() of any value on
  the <table> tag, not merely the absence of the 5px value the TRUE case
  emits, so an incorrectly emitted width of some other value now fails.
- override.borders: add the reciprocal control - custom.css containing
  "nth-child" with no "border" token anywhere leaves the border declaration
  in place. With the existing border-only case this pins that the heuristic
  requires both tokens; keying on either alone satisfies one case but not
  both.

Both were verified by mutation: dropping the "border" predicate from
override.borders, and emitting a different calc() width when
col.widths.fill.container is FALSE, each fail exactly one new assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SurreyHughesDisplayr

Copy link
Copy Markdown
Contributor Author

Both suppressed comments from the latest Copilot review are addressed in 22ec6e8.

col.widths.fill.container = FALSE (was line 992) — the negative assertion excluded only the exact 5px value the TRUE case emits, so a wrongly-emitted width of some other value would have passed. It now asserts the <table> tag carries no width:calc( at all. The height-side calc() positive control is unchanged.

override.borders (was line 1066) — added the reciprocal control: custom.css containing nth-child with the literal substring border absent anywhere leaves the border declaration in place. With the existing border-only case, the pair now pins that both tokens are required; keying on either one alone satisfies one case but not the other.

Both verified by mutation against R/createcustomtable.R:

  • dropping the border predicate (override.borders <- grepl("nth-child", custom.css, fixed = TRUE)) fails only the new nth-child-only assertion;
  • emitting a different width when the argument is FALSE (else "width:calc(100% - 1px)") fails only the new width:calc( assertion.

Source reverted after each. Suite: 248 passing, 0 failures, 0 warnings.

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

Test-only PR, suite is green (FAIL 0 | WARN 0 | SKIP 0 | PASS 248). Most of the new coverage is load-bearing — the override.borders block pins both tokens, and the col.widths.fill.container TRUE/FALSE pair is not vacuous. Three assertions are weaker than their comments claim; details inline. (Separately, a few of these tests brush past real product defects in R/createcustomtable.R — those will go in their own PR rather than here.)

Comment thread tests/testthat/test-createcustomtable.R Outdated
Comment on lines +976 to +979
# argument, not by the height-side calc() which reuses the same value and would
# otherwise make a default-offset assertion pass regardless of which mechanism produced it
res <- CreateCustomTable(x2, cell.border.width = 5, col.widths.fill.container = TRUE)
h <- normWs(tableHtml(res))

@chschan chschan Aug 26, 2026

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 comment claims cell.border.width = 5 pins the offset as coming from the width-side calc rather than the height-side one, but a scalar can't distinguish them. Width uses max(0, max(cell.border.width)) (R/createcustomtable.R:653), height uses rev(cell.border.width)[1] (line 651) — with a scalar both render 5px, so the assertion passes whichever mechanism produced it.

cell.border.width = c(5, 2) emits width:calc(100% - 5px); height:calc(100% - 2px), which does pin it: a refactor swapping max() for rev()[1] (or vice versa) would then fail. Worth changing the FALSE case at L988 to the same vector so the height:calc(100% - 2px) positive control at L997 stays aligned, and updating the comment to say what it's actually distinguishing.

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 fixed in 2c05983. With a scalar both sides render 5px, so neither assertion pinned its own mechanism. Both cases now use cell.border.width = c(5, 2): the TRUE case asserts width:calc(100% - 5px) (max) and height:calc(100% - 2px) (rev[1]), the FALSE case keeps the aligned height:calc(100% - 2px) control. Mutation-checked — setting table.width.offset <- rev(cell.border.width)[1] now fails the width assertion, which it did not before.

Comment on lines +949 to +959
{
resString <- CreateCustomTable(x2, col.widths = "20%, 30%, 50%")
resVector <- CreateCustomTable(x2, col.widths = c("20%", "30%", "50%"))
hString <- tableHtml(resString)
tagsString <- regmatches(hString, gregexpr("<col[^>]*>", hString))[[1]]
hVector <- tableHtml(resVector)
tagsVector <- regmatches(hVector, gregexpr("<col[^>]*>", hVector))[[1]]
expect_equal(tagsVector, c("<col width=' 20% '>", "<col width=' 30% '>", "<col width=' 50% '>"))
expect_equal(tagsString, tagsVector)
})

@chschan chschan Aug 26, 2026

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.

Only the vector leg is new. The test at L940 already pins tagsString against this same literal vector for "20%, 30%, 50%", so expect_equal(tagsVector, <literal>) plus expect_equal(tagsString, tagsVector) just re-asserts it.

Dropping resString/hString/tagsString (L950, L952-953, L957) leaves expect_equal(tagsVector, c(...)), which is the whole point of the test and still fails if vector input stops being handled equivalently.

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.

Right — dropped the string leg in 2c05983. The block is now just the vector call asserted against the literal, with a comment noting the literal is the one the comma-separated test above pins, which is what makes it an equivalence rather than a standalone assertion.

Comment thread tests/testthat/test-createcustomtable.R Outdated
Comment on lines +1038 to +1047
{
res <- CreateCustomTable(x2)
expect_true(attr(res, "can-run-in-root-dom"))
expect_false(grepl("mytesttoken", tableHtml(res), fixed = TRUE))

# positive control: the same distinctive token IS present when supplied via custom.css,
# so the negative assertion above isn't vacuous
resWithCss <- CreateCustomTable(x2, custom.css = "table.mytesttoken { color:red }")
expect_true(grepl("mytesttoken", tableHtml(resWithCss), fixed = TRUE))
})

@chschan chschan Aug 26, 2026

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 block has no failure mode:

  • L1041 — mytesttoken is a string invented by the test, so no regression in custom.css handling can make it appear in a default render. expect_false can't fail.
  • L1040 — duplicates the pre-existing iframes test (L22-23), which asserts the same can-run-in-root-dom TRUE on a default render (different fixture, same assertion) and also already covers the custom.cssNULL half.
  • L1045-1046 — duplicates the test at L1030, which already pins that supplied custom.css text reaches the HTML verbatim.

Suggest dropping the whole test_that. If the intent was to pin that the default custom.css = "" emits no user rule at all, assert against the actual markup (e.g. no extra rule beyond the generated .celldefault*/th/table set) rather than against a token the test made up.

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 on all three points — the whole test_that is deleted in 2c05983. mytesttoken is the test's own invention so the expect_false had no failure mode, and the other two halves were already covered by the iframes test and by the verbatim-custom.css test immediately above. I did not add the stronger "no rule beyond the generated set" version here: it would pin the whole predefined-CSS block, which is a separate concern from this plan's arguments.

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

Two coverage gaps, both cheap to close from tests already in this diff.

Comment thread tests/testthat/test-createcustomtable.R Outdated
Comment on lines +1094 to +1107
test_that("override.borders also suppresses the border declaration in colheaderdefault and rowheaderdefault",
{
cssBoth <- "table.mycustom { border: 4px solid rgb(9,9,9); } .mycustom:nth-child(2) { color:red; }"
res <- CreateCustomTable(x2, custom.css = cssBoth, col.header.border.width = 4, col.header.border.color = "red",
row.header.border.width = 4, row.header.border.color = "blue")
h <- normWs(tableHtml(res))

colHdrRule <- regmatches(h, regexpr("\\.colheaderdefault1\\{[^}]*\\}", h))
expect_length(colHdrRule, 1)
expect_false(grepl("border: 4px solid red", colHdrRule, fixed = TRUE))

rowHdrRule <- regmatches(h, regexpr("\\.rowheaderdefault1\\{[^}]*\\}", h))
expect_length(rowHdrRule, 1)
expect_false(grepl("border: 4px solid blue", rowHdrRule, fixed = 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.

override.borders gates six emission sites, and this PR covers three: celldefault (R/createcustomtable.R:473, previous test), rowheaderdefault (line 493) and colheaderdefault (line 558). Untested: rowspandefault (line 527), cornerdefault (line 574), colspandefault (line 606).

cornerdefault is the cheap one — it's emitted for this exact fixture (x2 has both dimnames, and the block at line 570 only requires show.row.headers), so it's one more regmatches in this test:

res <- CreateCustomTable(x2, custom.css = cssBoth, col.header.border.width = 4, col.header.border.color = "red",
            row.header.border.width = 4, row.header.border.color = "blue",
            corner.border.width = 4, corner.border.color = "green")
...
cornerRule <- regmatches(h, regexpr("\.cornerdefault1\{[^}]*\}", h))
expect_length(cornerRule, 1)
expect_false(grepl("border: 4px solid green", cornerRule, fixed = TRUE))

plus the matching positive control against resNoCss at L1111. The two span rules need a spanned fixture, so those are reasonable to leave — but worth naming them in the test title (or a comment) so this doesn't read as covering everything override.borders touches.

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.

Added in 2c05983corner.border.width = 4, corner.border.color = "green" on both the custom.css call and the resNoCss positive control, with the cornerdefault1 rule asserted both ways. Confirmed against the emitted CSS: suppressed with cssBoth, border: 4px solid green present without it. Title now names all three rules, and a comment states that rowspandefault and colspandefault are the two gated sites this file does not cover, since they need a spanned fixture.

Comment on lines +1120 to +1126
test_that("Enabling scroll forces the plain Box host even with no custom.css",
{
# row.height sets enable.y.scroll, which alone (custom.css still '') switches the
# widget host away from boxIframeless()
res <- CreateCustomTable(x2, row.height = "40px")
expect_equal(attr(res, "can-run-in-root-dom"), NULL)
})

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 reaches enable.scroll only through the y-side (row.heightenable.y.scroll), so the enable.x.scroll || term in enable.scroll <- enable.x.scroll || enable.y.scroll (R/createcustomtable.R:634) is untested — deleting that term still passes the whole suite. It would break two things at once: the wrong host is chosen at line 708, and the div#outer-table-container overflow rule (lines 640-641) is never emitted.

One line closes it:

expect_equal(attr(CreateCustomTable(x2, enable.x.scroll = TRUE), "can-run-in-root-dom"), NULL)

Asserting overflow-x: auto in the emitted div#outer-table-container rule pins it more precisely, if you want the stronger version.

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.

Took the stronger version in 2c05983: enable.x.scroll = TRUE asserts the NULL host attribute, plus the emitted div#outer-table-container rule containing overflow-x: auto and overflow-y: hidden — the y half pins which axis the argument actually drove. Mutation-checked: enable.scroll <- enable.y.scroll (first term deleted) now fails 4 assertions; before this it passed the whole suite.

From chschan's review of PR #71:

- col.widths.fill.container: cell.border.width = 5 could not distinguish the
  width-side offset (max(cell.border.width)) from the height-side one
  (rev(cell.border.width)[1]) - both render 5px. Use c(5, 2) in both the TRUE
  and FALSE cases so width pins 5px and height pins 2px, and swapping the two
  expressions fails the test.
- Drop the string leg of the character-vector col.widths test: the preceding
  test already pins the same literal vector for "20%, 30%, 50%", so it only
  re-asserted it. The vector assertion alone still fails if vector input stops
  being handled equivalently.
- Delete the "distinctive custom.css token" test: mytesttoken is invented by
  the test, so no regression could make it appear and the expect_false could
  not fail. Both of its other halves are covered - the iframes test pins
  can-run-in-root-dom for the default and the custom.css cases, and the
  preceding test pins that custom.css text reaches the HTML verbatim.
- Cover cornerdefault in the override.borders header test (the fixture already
  emits it) and name rowspandefault/colspandefault in a comment as the two
  gated sites this file does not cover - they need a spanned fixture.
- Cover the x side of 'enable.scroll <- enable.x.scroll || enable.y.scroll':
  the block only reached it through row.height, so the first term could be
  deleted with the suite still green. Also assert the emitted
  div#outer-table-container overflow rule.

Verified by mutation: computing the width offset with rev()[1] fails the new
5px/2px pair, and dropping the enable.x.scroll term fails the new x-scroll
assertions. Source reverted after each. Suite 252 passing, 0 failures.

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 b3a82bd into master Aug 26, 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