diff --git a/NEWS.md b/NEWS.md index ec55e63d8..8c5593baf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -40,6 +40,10 @@ - Improve common chunk detection, output `na_group` and `row_in_group` when there are missing values. +# Changes in version 2025.10.6 (PR#246) + +- Added validation for selector names to prevent browser rendering failures. Selector names (from data values used in `clickSelects` and `showSelected`) cannot contain CSS special characters like `#`, `@`, `!`, `$`, etc., as these interfere with JavaScript DOM selectors and cause blank visualizations in the browser. The compiler now stops with a clear error message identifying problematic selector names, helping users fix data issues before attempting to render. + # Changes in version 2025.10.3 (PR#240) - `guide_legend(override.aes)` works in a plot with both color and fill legends. diff --git a/R/z_animint.R b/R/z_animint.R index 3beb7dd7c..c50bfee02 100644 --- a/R/z_animint.R +++ b/R/z_animint.R @@ -438,6 +438,9 @@ animint2dir <- function ## For a static data viz with no interactive aes, no need to check ## for trivial showSelected variables with only 1 level. checkSingleShowSelectedValue(meta$selectors) + + ## Check selector names for CSS compatibility (no special characters like #) + checkSelectorNames(meta$selectors) ## Go through options and add to the list. for(v.name in names(meta$duration)){ diff --git a/R/z_animintHelpers.R b/R/z_animintHelpers.R index f36864547..97cce5a7f 100644 --- a/R/z_animintHelpers.R +++ b/R/z_animintHelpers.R @@ -607,6 +607,22 @@ checkSingleShowSelectedValue <- function(selectors){ } +#' Validate selector names for CSS compatibility +#' @param selectors selectors to validate +#' @return \code{NULL}. Throws error if invalid characters found. +checkSelectorNames <- function(selectors){ + selector.names <- names(selectors) + ## Characters that are invalid in CSS selectors and cause issues in browser + ## ] must be first in character class, [ can be anywhere after + invalid.pattern <- "[][#!@$%^&*=|/'\"`?<>()\\\\]" + has.invalid <- grepl(invalid.pattern, selector.names) + if(any(has.invalid)){ + invalid.names <- selector.names[has.invalid] + stop(sprintf("Invalid character(s) in selector name(s).\nSelector names cannot contain special characters that interfere with CSS selectors.\nThe following selector(s) contain invalid characters:\n%s\n\nPlease remove or replace these characters in your variable names.", paste("-", invalid.names, collapse="\n"))) + } +} + + #' Set plot width and height for all plots #' @param meta meta object with all information #' @param AllPlotsInfo plot info list diff --git a/tests/testthat/test-compiler-invalid-selector-characters.R b/tests/testthat/test-compiler-invalid-selector-characters.R new file mode 100644 index 000000000..29a5594de --- /dev/null +++ b/tests/testthat/test-compiler-invalid-selector-characters.R @@ -0,0 +1,130 @@ +context("Invalid selector character validation") + +# Test that selector names with special characters cause errors +# Note: Selector names come from the VALUES in the data when using .variable/.value pattern +test_that("selector name with # causes error in clickSelects", { + viz <- list( + plot1 = ggplot() + + geom_point( + aes(Sepal.Length, Petal.Length), + data = data.frame( + Sepal.Length = 1:10, + Petal.Length = rnorm(10), + regularization = "# nearest neighbors", # This VALUE becomes the selector name + parameter = 1:10 + ), + clickSelects = c(regularization = "parameter") + ) + ) + expect_error( + animint2dir(viz, open.browser = FALSE), + "Invalid character(s) in selector name(s). +Selector names cannot contain special characters that interfere with CSS selectors. +The following selector(s) contain invalid characters: +- # nearest neighbors + +Please remove or replace these characters in your variable names.", + fixed = TRUE + ) +}) + +test_that("selector name with @ causes error in clickSelects", { + viz <- list( + plot1 = ggplot() + + geom_point( + aes(Sepal.Length, Petal.Length), + data = data.frame( + Sepal.Length = 1:10, + Petal.Length = rnorm(10), + regularization = "model@version1", # This VALUE becomes selector name + parameter = 1:10 + ), + clickSelects = c(regularization = "parameter") + ) + ) + expect_error( + animint2dir(viz, open.browser = FALSE), + "Invalid character(s) in selector name(s). +Selector names cannot contain special characters that interfere with CSS selectors. +The following selector(s) contain invalid characters: +- model@version1 + +Please remove or replace these characters in your variable names.", + fixed = TRUE + ) +}) + +test_that("selector name with ! causes error", { + viz <- list( + plot1 = ggplot() + + geom_point( + aes(x=1:10, y=rnorm(10)), + data = data.frame( + model = "model!important", + parameter = 1:10 + ), + clickSelects = c(model = "parameter") + ) + ) + expect_error( + animint2dir(viz, open.browser = FALSE), + "Invalid character(s) in selector name(s). +Selector names cannot contain special characters that interfere with CSS selectors. +The following selector(s) contain invalid characters: +- model!important + +Please remove or replace these characters in your variable names.", + fixed = TRUE + ) +}) + +test_that("valid selector names work with clickSelects", { + viz <- list( + plot1 = ggplot() + + geom_point( + aes(x=1:10, y=rnorm(10)), + data = data.frame( + regularization = "polynomial_degree", # Valid name + parameter = 0:9 + ), + clickSelects = c(regularization = "parameter") + ) + ) + info <- animint2dir(viz, open.browser = FALSE) + expect_true(TRUE, info = "Valid selector names should not cause errors") +}) + +test_that("selector names with spaces work", { + viz <- list( + plot1 = ggplot() + + geom_point( + aes(x=1:10, y=rnorm(10)), + data = data.frame( + regularization = "nearest neighbors", # spaces are OK + parameter = 1:10 + ), + clickSelects = c(regularization = "parameter") + ) + ) + info <- animint2dir(viz, open.browser = FALSE) + expect_true(TRUE, info = "Selector names with spaces should work") +}) + +test_that("multiple values with invalid characters all reported", { + viz <- list( + plot1 = ggplot() + + geom_point( + aes(x=1:10, y=rnorm(10)), + data = data.frame( + regularization = rep(c("#bad", "!worse"), 5), # Both have invalid chars + parameter = 1:10 + ), + clickSelects = c(regularization = "parameter") + ) + ) + expect_error( + animint2dir(viz, open.browser = FALSE), + "Invalid character(s) in selector name(s).\nSelector names cannot contain special characters that interfere with CSS selectors.\nThe following selector(s) contain invalid characters:\n- #bad\n- !worse\n\nPlease remove or replace these characters in your variable names.", + fixed = TRUE + ) +})