RS-21803: Col Widths and Custom CSS test - #71
Conversation
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>
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
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
borderwithoutnth-child. If the implementation dropped theborderpredicate and keyed solely onnth-child, every assertion here would still pass. Add the reciprocalnth-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
5pxvalue, so the test still passes if disablingcol.widths.fill.containerincorrectly emits anotherwidth: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>
|
Both suppressed comments from the latest Copilot review are addressed in 22ec6e8.
Both verified by mutation against
Source reverted after each. Suite: 248 passing, 0 failures, 0 warnings. |
chschan
left a comment
There was a problem hiding this comment.
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.)
| # 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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| { | ||
| 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) | ||
| }) | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| { | ||
| 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)) | ||
| }) |
There was a problem hiding this comment.
This block has no failure mode:
- L1041 —
mytesttokenis a string invented by the test, so no regression incustom.csshandling can make it appear in a default render.expect_falsecan't fail. - L1040 — duplicates the pre-existing
iframestest (L22-23), which asserts the samecan-run-in-root-domTRUE on a default render (different fixture, same assertion) and also already covers thecustom.css→NULLhalf. - L1045-1046 — duplicates the test at L1030, which already pins that supplied
custom.csstext 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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Two coverage gaps, both cheap to close from tests already in this diff.
| 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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Added in 2c05983 — corner.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.
| 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) | ||
| }) |
There was a problem hiding this comment.
This reaches enable.scroll only through the y-side (row.height → enable.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.
There was a problem hiding this comment.
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>
Summary
Adds unit test coverage for
col.widthsand custom CSS inCreateCustomTable(R/createcustomtable.R) — 13 test blocks.<col>tags exactly, including the literal surrounding spaces thatpaste(..., sep = " ")produces:<col width=' 200px '>. Percentage widths are pinned as an explicit vector so the assertion fails ifcol.widthsis ignored and the default25%is emitted instead.col.widthscounting 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.containeron and off, with the extracted<table>tag length-guarded so an empty match cannot pass vacuously.calc()table-width offset trackscell.border.width, pinned at a non-defaultc(5, 2): the width side takesmax()= 5 and the height siderev()[1]= 2, so each assertion fails if the two expressions are swapped. A scalar could not tell them apart.row.height(y scroll) orenable.x.scrollswitches the widget fromboxIframelessto a plainBox, and the x case also pins the emitteddiv#outer-table-containeroverflow rule. Thecustom.csshalf is already covered by the pre-existingiframestest.override.borders, including the case that pins its substring match as genuinely unanchored — an unrelated.x:nth-child(2)rule alongsideborder-topsuppresses 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 nowmaster. Earlier plans in the stack (#66, #67, #68, #69, #70) are all merged; the diff here is this plan's work only.Test plan
252 passing, 0 failures, 0 warnings.
🤖 Generated with Claude Code