From ffa44db16c3a59d64dbc23a868abcae4faa9cdb1 Mon Sep 17 00:00:00 2001 From: Surrey Date: Fri, 21 Aug 2026 10:19:56 +1000 Subject: [PATCH 1/3] test: cover banded.rows/banded.cols in CreateCustomTable Adds unit tests for CreateCustomTable's banded.rows/banded.cols behaviour: no CSS emitted by default, the odd/even row-banding rule (pinning the confirmed unscoped tr:nth-child(even) clause), custom fill colours, per-cell background suppression shared by both flags, the verbatim td:nth-child(2n+3)/even column rule, both flags together, and row-count independence of the emitted rule count. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/testthat/test-createcustomtable.R | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/testthat/test-createcustomtable.R b/tests/testthat/test-createcustomtable.R index 4b775f9..1288d5f 100644 --- a/tests/testthat/test-createcustomtable.R +++ b/tests/testthat/test-createcustomtable.R @@ -524,6 +524,87 @@ test_that("A multi-span over-cover silently drops the last span and leaks a cbin expect_false(grepl("rowspan", rows[4], fixed = TRUE)) }) +# banded.rows / banded.cols ------------------------------------------------ + +test_that("Default emits no banding CSS", +{ + res <- CreateCustomTable(x2) + expect_false(grepl("nth-child", tableHtml(res), fixed = TRUE)) +}) + +test_that("banded.rows = TRUE emits the odd/even row rule with default fills, including the unscoped even clause", +{ + # cata() joins its arguments with a space (its default cat() sep), so the emitted + # declaration has spaces around the fill value even though the source concatenates + # 'background-color:' and the fill value as adjacent cata() arguments; normWs + # collapses runs of whitespace to a single space rather than removing it, so those + # spaces remain in the string pinned below + res <- CreateCustomTable(x2, banded.rows = TRUE) + h <- normWs(tableHtml(res)) + expect_true(grepl("tbody tr:nth-child(odd){background-color: rgb(250,250,250) ;}", h, fixed = TRUE)) + expect_true(grepl("tr:nth-child(even){background-color: rgb(245,245,245) ;}", h, fixed = TRUE)) + + # the odd rule is scoped to the container selector, but the even clause that follows + # it is not re-prefixed - pinning the confirmed selector-scoping gap + containerSel <- regmatches(h, regexpr("[.]custom-table-container-[A-Za-z0-9_-]+", h, perl = TRUE)) + expect_true(grepl(paste0(containerSel, " tbody tr:nth-child(odd)"), h, fixed = TRUE)) + beforeEven <- substr(h, 1, regexpr("tr:nth-child(even)", h, fixed = TRUE) - 1) + afterOddRule <- sub(".*tr:nth-child\\(odd\\)\\{[^}]*\\}", "", beforeEven) + expect_false(grepl(containerSel, afterOddRule, fixed = TRUE)) +}) + +test_that("Custom banded.odd.fill and banded.even.fill values are used instead of the defaults", +{ + res <- CreateCustomTable(x2, banded.rows = TRUE, + banded.odd.fill = "rgb(1,1,1)", banded.even.fill = "rgb(2,2,2)") + h <- normWs(tableHtml(res)) + expect_true(grepl("rgb(1,1,1)", h, fixed = TRUE)) + expect_true(grepl("rgb(2,2,2)", h, fixed = TRUE)) + expect_false(grepl("rgb(250,250,250)", h, fixed = TRUE)) + expect_false(grepl("rgb(245,245,245)", h, fixed = TRUE)) +}) + +test_that("banded.rows = TRUE drops the per-cell background from the celldefault CSS", +{ + resBanded <- CreateCustomTable(x2, cell.fill = "rgb(3,3,3)", banded.rows = TRUE) + hBanded <- normWs(tableHtml(resBanded)) + expect_false(grepl("background: rgb(3,3,3)", hBanded, fixed = TRUE)) + + # contrast: with banded.rows = FALSE (the only argument that differs), the same + # cell.fill value does reach the celldefault declaration + resFlat <- CreateCustomTable(x2, cell.fill = "rgb(3,3,3)", banded.rows = FALSE) + hFlat <- normWs(tableHtml(resFlat)) + expect_true(grepl("background: rgb(3,3,3)", hFlat, fixed = TRUE)) +}) + +test_that("banded.cols = TRUE also suppresses the cell fill and emits the column rule verbatim", +{ + # pinning the literal 2n+3 / even selector pair as coded (see plan resolved question 1); + # not asserting a "corrected" odd/even split + res <- CreateCustomTable(x2, cell.fill = "rgb(3,3,3)", banded.cols = TRUE) + h <- normWs(tableHtml(res)) + expect_true(grepl("tbody td:nth-child(2n+3){background-color:", h, fixed = TRUE)) + expect_false(grepl("background: rgb(3,3,3)", h, fixed = TRUE)) +}) + +test_that("banded.rows and banded.cols together emit both the row and column banding rules", +{ + res <- CreateCustomTable(x2, banded.rows = TRUE, banded.cols = TRUE) + h <- normWs(tableHtml(res)) + expect_true(grepl("tr:nth-child(odd)", h, fixed = TRUE)) + expect_true(grepl("td:nth-child(2n+3)", h, fixed = TRUE)) +}) + +test_that("Row count does not change how many times the banding rule is emitted", +{ + m2 <- matrix(1:2, 2, 1, dimnames = list(c("a", "b"), "X")) + m7 <- matrix(1:7, 7, 1, dimnames = list(letters[1:7], "X")) + res2 <- CreateCustomTable(m2, banded.rows = TRUE) + res7 <- CreateCustomTable(m7, banded.rows = TRUE) + expect_equal(countOccurrences("nth-child(odd)", tableHtml(res2)), 1) + expect_equal(countOccurrences("nth-child(odd)", tableHtml(res7)), 1) +}) + test_that("All-height-1 row.spans with sticky positioning errors on invalid unary -NULL (genuine unticketed defect)", { # Genuine production crash being pinned here (not yet ticketed): rm.index is only From 1e20f9278f327015de6004ba06048db17c6b5f50 Mon Sep 17 00:00:00 2001 From: Surrey Date: Fri, 21 Aug 2026 10:31:22 +1000 Subject: [PATCH 2/3] fix(test): pin full banded.cols rule, fix scoping guard, and correct plan-mistake comment Pins the whole banded.cols verbatim CSS rule (both the 2n+3 and even clauses with their fill values) instead of only half of it, and adds a scoping-leak check mirroring the existing banded.rows one, confirming the unscoped `td:nth-child(even)` clause is a real, unticketed defect. Removes the incorrect "defect"/"corrected odd/even split" framing for the 2n+3/even selector pair, which is disjoint and correct as coded - that was a plan-authoring mistake, not a production bug. Guards the container-selector regex extraction with expect_length() so a failed match can't pass vacuously. Pins the paired custom banded.odd.fill / banded.even.fill values so swapping them would fail. Tightens two cell-fill assertions to include the trailing " ;" for anchoring. Moves the banded.rows/banded.cols section to after the last row.spans test it had been accidentally inserted into the middle of. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/testthat/test-createcustomtable.R | 111 ++++++++++++++---------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/tests/testthat/test-createcustomtable.R b/tests/testthat/test-createcustomtable.R index 1288d5f..dd40e7e 100644 --- a/tests/testthat/test-createcustomtable.R +++ b/tests/testthat/test-createcustomtable.R @@ -524,6 +524,43 @@ test_that("A multi-span over-cover silently drops the last span and leaks a cbin expect_false(grepl("rowspan", rows[4], fixed = TRUE)) }) +test_that("All-height-1 row.spans with sticky positioning errors on invalid unary -NULL (genuine unticketed defect)", +{ + # Genuine production crash being pinned here (not yet ticketed): rm.index is only + # populated for spans taller than one row, so when EVERY span height is 1, rm.index + # stays NULL, and the pruning expression `top.position[-rm.index]` becomes + # `top.position[-NULL]`. In base R, unary minus on NULL is invalid ("invalid argument + # to unary operator"), so the function throws rather than returning the original + # top.position unchanged. This only happens when sticky positioning is also active + # (row.height set AND num.header.rows set), since top.position is only computed - and + # only fed into the row.spans pruning branch - in that case. If this defect is ever + # fixed, this expect_error should be changed to assert successful output instead. + allOnes <- list(list(height = 1, label = "AA"), list(height = 1, label = "BB"), + list(height = 1, label = "CC"), list(height = 1, label = "DD")) + expect_error( + CreateCustomTable(rowSpanMatrix, row.spans = allOnes, row.height = "30px", + num.header.rows = 1), + "invalid argument to unary operator", fixed = TRUE + ) + + # Same all-height-1 spans without sticky positioning: top.position/rm.index pruning is + # never reached, so this succeeds - the crash is specific to the sticky combination, + # not to row.spans (or all-height-1 spans) in general. + expect_error(res <- CreateCustomTable(rowSpanMatrix, row.spans = allOnes), NA) + expect_true(grepl('AA', tableHtml(res), + fixed = TRUE)) + + # Mixed heights (2, 1, 1) with the same sticky args: rm.index is populated (the height-2 + # span), so the pruning expression is a normal subscript, not `-NULL`, and this succeeds. + mixedHeights <- list(list(height = 2, label = "AA"), list(height = 1, label = "BB"), + list(height = 1, label = "CC")) + expect_error( + CreateCustomTable(rowSpanMatrix, row.spans = mixedHeights, row.height = "30px", + num.header.rows = 1), + NA + ) +}) + # banded.rows / banded.cols ------------------------------------------------ test_that("Default emits no banding CSS", @@ -547,6 +584,7 @@ test_that("banded.rows = TRUE emits the odd/even row rule with default fills, in # the odd rule is scoped to the container selector, but the even clause that follows # it is not re-prefixed - pinning the confirmed selector-scoping gap containerSel <- regmatches(h, regexpr("[.]custom-table-container-[A-Za-z0-9_-]+", h, perl = TRUE)) + expect_length(containerSel, 1) expect_true(grepl(paste0(containerSel, " tbody tr:nth-child(odd)"), h, fixed = TRUE)) beforeEven <- substr(h, 1, regexpr("tr:nth-child(even)", h, fixed = TRUE) - 1) afterOddRule <- sub(".*tr:nth-child\\(odd\\)\\{[^}]*\\}", "", beforeEven) @@ -558,8 +596,8 @@ test_that("Custom banded.odd.fill and banded.even.fill values are used instead o res <- CreateCustomTable(x2, banded.rows = TRUE, banded.odd.fill = "rgb(1,1,1)", banded.even.fill = "rgb(2,2,2)") h <- normWs(tableHtml(res)) - expect_true(grepl("rgb(1,1,1)", h, fixed = TRUE)) - expect_true(grepl("rgb(2,2,2)", h, fixed = TRUE)) + expect_true(grepl("tbody tr:nth-child(odd){background-color: rgb(1,1,1) ;}", h, fixed = TRUE)) + expect_true(grepl("tr:nth-child(even){background-color: rgb(2,2,2) ;}", h, fixed = TRUE)) expect_false(grepl("rgb(250,250,250)", h, fixed = TRUE)) expect_false(grepl("rgb(245,245,245)", h, fixed = TRUE)) }) @@ -568,23 +606,41 @@ test_that("banded.rows = TRUE drops the per-cell background from the celldefault { resBanded <- CreateCustomTable(x2, cell.fill = "rgb(3,3,3)", banded.rows = TRUE) hBanded <- normWs(tableHtml(resBanded)) - expect_false(grepl("background: rgb(3,3,3)", hBanded, fixed = TRUE)) + expect_false(grepl("background: rgb(3,3,3) ;", hBanded, fixed = TRUE)) # contrast: with banded.rows = FALSE (the only argument that differs), the same # cell.fill value does reach the celldefault declaration resFlat <- CreateCustomTable(x2, cell.fill = "rgb(3,3,3)", banded.rows = FALSE) hFlat <- normWs(tableHtml(resFlat)) - expect_true(grepl("background: rgb(3,3,3)", hFlat, fixed = TRUE)) + expect_true(grepl("background: rgb(3,3,3) ;", hFlat, fixed = TRUE)) }) -test_that("banded.cols = TRUE also suppresses the cell fill and emits the column rule verbatim", +test_that("banded.cols = TRUE also suppresses the cell fill and emits the full column rule verbatim", { - # pinning the literal 2n+3 / even selector pair as coded (see plan resolved question 1); - # not asserting a "corrected" odd/even split + # pinning the whole emitted column rule, both the 2n+3 clause and its even-selector + # continuation with its own fill value (see plan resolved question 1). 2n+3 yields + # {3,5,7,...} (odd) and even yields {2,4,6,...} - these are disjoint, and with the + # row-header cell occupying nth-child(1), the data columns land on 2 (even), 3 (2n+3), + # 4 (even), i.e. a correctly alternating band that deliberately excludes the header + # column. This is not a defect; the row/col naming just doesn't match odd/even parity + # once the header column is accounted for. (One genuine gap this leaves untested: with + # show.row.headers = FALSE, column 1 falls into neither selector and is left unbanded - + # not asserted here.) res <- CreateCustomTable(x2, cell.fill = "rgb(3,3,3)", banded.cols = TRUE) h <- normWs(tableHtml(res)) - expect_true(grepl("tbody td:nth-child(2n+3){background-color:", h, fixed = TRUE)) - expect_false(grepl("background: rgb(3,3,3)", h, fixed = TRUE)) + expect_true(grepl(paste0("tbody td:nth-child(2n+3){background-color: rgb(250,250,250) ;}", + " td:nth-child(even){background-color: rgb(245,245,245) ;}"), + h, fixed = TRUE)) + expect_false(grepl("background: rgb(3,3,3) ;", h, fixed = TRUE)) + + # mirrors the row-banding scoping check above: the 2n+3 clause is scoped to the + # container selector, but the even clause that follows it is not re-prefixed + containerSel <- regmatches(h, regexpr("[.]custom-table-container-[A-Za-z0-9_-]+", h, perl = TRUE)) + expect_length(containerSel, 1) + expect_true(grepl(paste0(containerSel, " tbody td:nth-child(2n+3)"), h, fixed = TRUE)) + beforeEven <- substr(h, 1, regexpr("td:nth-child(even)", h, fixed = TRUE) - 1) + afterOddRule <- sub(".*td:nth-child\\(2n[+]3\\)\\{[^}]*\\}", "", beforeEven) + expect_false(grepl(containerSel, afterOddRule, fixed = TRUE)) }) test_that("banded.rows and banded.cols together emit both the row and column banding rules", @@ -604,40 +660,3 @@ test_that("Row count does not change how many times the banding rule is emitted" expect_equal(countOccurrences("nth-child(odd)", tableHtml(res2)), 1) expect_equal(countOccurrences("nth-child(odd)", tableHtml(res7)), 1) }) - -test_that("All-height-1 row.spans with sticky positioning errors on invalid unary -NULL (genuine unticketed defect)", -{ - # Genuine production crash being pinned here (not yet ticketed): rm.index is only - # populated for spans taller than one row, so when EVERY span height is 1, rm.index - # stays NULL, and the pruning expression `top.position[-rm.index]` becomes - # `top.position[-NULL]`. In base R, unary minus on NULL is invalid ("invalid argument - # to unary operator"), so the function throws rather than returning the original - # top.position unchanged. This only happens when sticky positioning is also active - # (row.height set AND num.header.rows set), since top.position is only computed - and - # only fed into the row.spans pruning branch - in that case. If this defect is ever - # fixed, this expect_error should be changed to assert successful output instead. - allOnes <- list(list(height = 1, label = "AA"), list(height = 1, label = "BB"), - list(height = 1, label = "CC"), list(height = 1, label = "DD")) - expect_error( - CreateCustomTable(rowSpanMatrix, row.spans = allOnes, row.height = "30px", - num.header.rows = 1), - "invalid argument to unary operator", fixed = TRUE - ) - - # Same all-height-1 spans without sticky positioning: top.position/rm.index pruning is - # never reached, so this succeeds - the crash is specific to the sticky combination, - # not to row.spans (or all-height-1 spans) in general. - expect_error(res <- CreateCustomTable(rowSpanMatrix, row.spans = allOnes), NA) - expect_true(grepl('AA', tableHtml(res), - fixed = TRUE)) - - # Mixed heights (2, 1, 1) with the same sticky args: rm.index is populated (the height-2 - # span), so the pruning expression is a normal subscript, not `-NULL`, and this succeeds. - mixedHeights <- list(list(height = 2, label = "AA"), list(height = 1, label = "BB"), - list(height = 1, label = "CC")) - expect_error( - CreateCustomTable(rowSpanMatrix, row.spans = mixedHeights, row.height = "30px", - num.header.rows = 1), - NA - ) -}) From 84eb518c59a61662754ed33093ca942bcc25cc8c Mon Sep 17 00:00:00 2001 From: Surrey Date: Fri, 21 Aug 2026 10:45:11 +1000 Subject: [PATCH 3/3] test: pin banded row rule as one contiguous string Merge the odd/even grepl pairs for banded.rows (default and custom fill cases) into single contiguous-string assertions, mirroring the existing banded.cols test. As two independent greps, inserting content between the odd and even clauses in production would not have failed these assertions. Also drop "deliberately" from a comment asserting unestablished author intent. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/testthat/test-createcustomtable.R | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-createcustomtable.R b/tests/testthat/test-createcustomtable.R index dd40e7e..5ec99e2 100644 --- a/tests/testthat/test-createcustomtable.R +++ b/tests/testthat/test-createcustomtable.R @@ -578,8 +578,8 @@ test_that("banded.rows = TRUE emits the odd/even row rule with default fills, in # spaces remain in the string pinned below res <- CreateCustomTable(x2, banded.rows = TRUE) h <- normWs(tableHtml(res)) - expect_true(grepl("tbody tr:nth-child(odd){background-color: rgb(250,250,250) ;}", h, fixed = TRUE)) - expect_true(grepl("tr:nth-child(even){background-color: rgb(245,245,245) ;}", h, fixed = TRUE)) + expect_true(grepl(paste0("tbody tr:nth-child(odd){background-color: rgb(250,250,250) ;}", + " tr:nth-child(even){background-color: rgb(245,245,245) ;}"), h, fixed = TRUE)) # the odd rule is scoped to the container selector, but the even clause that follows # it is not re-prefixed - pinning the confirmed selector-scoping gap @@ -596,8 +596,8 @@ test_that("Custom banded.odd.fill and banded.even.fill values are used instead o res <- CreateCustomTable(x2, banded.rows = TRUE, banded.odd.fill = "rgb(1,1,1)", banded.even.fill = "rgb(2,2,2)") h <- normWs(tableHtml(res)) - expect_true(grepl("tbody tr:nth-child(odd){background-color: rgb(1,1,1) ;}", h, fixed = TRUE)) - expect_true(grepl("tr:nth-child(even){background-color: rgb(2,2,2) ;}", h, fixed = TRUE)) + expect_true(grepl(paste0("tbody tr:nth-child(odd){background-color: rgb(1,1,1) ;}", + " tr:nth-child(even){background-color: rgb(2,2,2) ;}"), h, fixed = TRUE)) expect_false(grepl("rgb(250,250,250)", h, fixed = TRUE)) expect_false(grepl("rgb(245,245,245)", h, fixed = TRUE)) }) @@ -621,7 +621,7 @@ test_that("banded.cols = TRUE also suppresses the cell fill and emits the full c # continuation with its own fill value (see plan resolved question 1). 2n+3 yields # {3,5,7,...} (odd) and even yields {2,4,6,...} - these are disjoint, and with the # row-header cell occupying nth-child(1), the data columns land on 2 (even), 3 (2n+3), - # 4 (even), i.e. a correctly alternating band that deliberately excludes the header + # 4 (even), i.e. a correctly alternating band that excludes the header # column. This is not a defect; the row/col naming just doesn't match odd/even parity # once the header column is accounted for. (One genuine gap this leaves untested: with # show.row.headers = FALSE, column 1 falls into neither selector and is left unbanded -