diff --git a/DESCRIPTION b/DESCRIPTION index 5726ac9..afed027 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -20,4 +20,5 @@ Suggests: rmarkdown, testthat (>= 3.0.0) VignetteBuilder: knitr +Roxygen: list(markdown = TRUE) Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index 78b6cc1..06727ff 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,7 +6,7 @@ export(init) export(on) export(trigger) export(watch) -importFrom(attempt,stop_if) +importFrom(attempt,stop_if_not) importFrom(shiny,getDefaultReactiveDomain) importFrom(shiny,observeEvent) importFrom(shiny,reactiveVal) diff --git a/R/funs.R b/R/funs.R index ce25248..b76d67f 100644 --- a/R/funs.R +++ b/R/funs.R @@ -11,21 +11,20 @@ #' @importFrom shiny reactiveVal #' @export #' @examples -#' if (interactive()){ +#' if (interactive()) { #' library(shiny) #' library(gargoyle) #' options("gargoyle.talkative" = TRUE) -#' ui <- function(request){ +#' ui <- function(request) { #' tagList( -#' h4('Go'), +#' h4("Go"), #' actionButton("y", "y"), -#' h4('Output of z$v'), +#' h4("Output of z$v"), #' tableOutput("evt") #' ) #' } #' -#' server <- function(input, output, session){ -#' +#' server <- function(input, output, session) { #' # Initiating the flags #' init("airquality", "iris", "renderiris") #' @@ -33,7 +32,7 @@ #' # a reactive structure #' z <- new.env() #' -#' observeEvent( input$y , { +#' observeEvent(input$y, { #' z$v <- mtcars #' # Triggering the flag #' trigger("airquality") @@ -57,25 +56,22 @@ #' watch("renderiris") #' head(z$v) #' }) -#' #' } #' #' shinyApp(ui, server) -#' #' } -init <- function(..., session = getDefaultReactiveDomain()){ +init <- function(..., session = getDefaultReactiveDomain()) { lapply( list(...), - function(x){ + function(x) { session$userData[[x]] <- reactiveVal(0) } ) - } #' @rdname Event #' @export -trigger <- function(..., session = getDefaultReactiveDomain()){ +trigger <- function(..., session = getDefaultReactiveDomain()) { .logs$log <- rbind( .logs$log, data.frame( @@ -86,8 +82,8 @@ trigger <- function(..., session = getDefaultReactiveDomain()){ ) lapply( list(...), - function(x){ - if (getOption("gargoyle.talkative", FALSE)){ + function(x) { + if (getOption("gargoyle.talkative", FALSE)) { cat( "- [Gargoyle] Triggering", x, @@ -99,44 +95,43 @@ trigger <- function(..., session = getDefaultReactiveDomain()){ ) } ) - } #' @rdname Event #' @export -watch <- function(name, session = getDefaultReactiveDomain()){ +watch <- function(name, session = getDefaultReactiveDomain()) { session$userData[[name]]() } - #' React on an event #' -#' @param name the name of the event to react to -#' @param expr the expression to run when the event -#' is triggered. -#' @param session The shiny session object +#' @param name The name of the event to react to as a character; can be a +#' character vector of event names in which case a reaction is triggered if +#' any (all) of the events is (are) triggered (i.e. the non-exclusive "OR" +#' case). +#' @param expr The expression to run when the event is triggered. +#' @param session The shiny session object. #' -#' @return An observeEvent object. This object will +#' @return An [shiny::observeEvent()] object. This object will #' rarely be used, `on` is mainly called for side-effects. #' #' @export #' @importFrom shiny observeEvent getDefaultReactiveDomain -#' @importFrom attempt stop_if +#' @importFrom attempt stop_if_not on <- function( name, expr, session = getDefaultReactiveDomain() - ){ - - stop_if( - session$userData[[name]], - is.null, - sprintf( - "[Gargoyle] Flag %s hasn't been initiated: can't listen to it.", - name + ) { + stop_if_not( + all(name %in% names(session$userData)), + msg = sprintf( + "[Gargoyle] Flag %s hasn't been initiated: can't listen to it.\n", + name[!(name %in% names(session$userData))] ) ) + watch_expr <- generate_watch_expr(name) observeEvent( - substitute(gargoyle::watch(name)), + do.call("substitute", list(watch_expr[[1]])), { substitute(expr) }, @@ -147,3 +142,22 @@ on <- function( handler.env = parent.frame() ) } +generate_watch_expr <- function(name) { + if (length(name) > 1) { + tmp_expr <- parse( + text = paste0( + "list(", + paste0( + "gargoyle::watch(name[", + seq_along(name), + "], session = session)", + collapse = ", " + ), + ")" + ) + ) + } else { + tmp_expr <- parse(text = "gargoyle::watch(name, session = session)") + } + return(tmp_expr) +} diff --git a/tests/testthat/test-funs.R b/tests/testthat/test-funs.R index 737a63e..40e7d2d 100644 --- a/tests/testthat/test-funs.R +++ b/tests/testthat/test-funs.R @@ -25,3 +25,247 @@ test_that("gargoyle works", { shiny::reactiveConsole(FALSE) }) +test_that("gargoyle on() works for name vector", { + shiny::reactiveConsole(TRUE) + + s <- shiny::MockShinySession$new() + + init("pif_01", "pif_02", "pif_03", session = s) + + # 0. Define on() for 'name' vector + # 0.A for pif_01 and pif_02 + on( + c("pif_01", "pif_02"), + { + cat("Event on-Type 1: triggered by pif_{01,02}!\n") + }, + session = s + ) + expect_true( + s$userData$pif_01() == 0 + ) + expect_true( + s$userData$pif_02() == 0 + ) + # 0.B for pif_02 and pif_03 + on( + c("pif_02", "pif_03"), + { + cat("Event on-Type 2: triggered by pif_{02,03}!\n") + }, + session = s + ) + expect_true( + s$userData$pif_02() == 0 + ) + expect_true( + s$userData$pif_03() == 0 + ) + # I. Test single trigger + trigger("pif_01", session = s) + trigger("pif_03", session = s) + expect_true( + s$userData$pif_01() == 1 + ) + expect_true( + s$userData$pif_02() == 0 + ) + expect_true( + s$userData$pif_03() == 1 + ) + + # II. Test double trigger + trigger("pif_02", session = s) + trigger("pif_01", "pif_03", session = s) + expect_true( + s$userData$pif_01() == 2 + ) + expect_true( + s$userData$pif_02() == 1 + ) + expect_true( + s$userData$pif_03() == 2 + ) + + # II. Run tests for on() with errors - uninitialized events + # II.A Provoke error due to uninitialized event(s) - mismatch from init(s) + # first event exists - second does not + expect_error( + on( + c("pif_01", "pif_2"), + { + cat(1 + 1) + }, + session = s + ) + ) + # first does not exits - second event exists + expect_error( + on( + c("pif_1", "pif_02"), + { + cat(1 + 1) + }, + session = s + ) + ) + # both events do not exists + expect_error( + on( + c("pif_1", "pif_2"), + { + cat(1 + 1) + }, + session = s + ) + ) + # II.B Check error message of uninitialized event(s) - mismatch from init(s) + # single mismatch for second event + out_error <- try( + on( + c("pif_01", "pif_2"), + { + cat(1 + 1) + }, + session = s + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + "Error : [Gargoyle] Flag pif_2 hasn't been initiated: can't listen to it.\n\n" + ) + # single mismatch for first event + out_error <- try( + on( + c("pif_1", "pif_02"), + { + cat(1 + 1) + }, + session = s + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + "Error : [Gargoyle] Flag pif_1 hasn't been initiated: can't listen to it.\n\n" + ) + # double mismatch for first event + out_error <- try( + on( + c("pif_1", "pif_2"), + { + cat(1 + 1) + }, + session = s + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + paste0( + "Error : [Gargoyle] Flag pif_1 hasn't been initiated: can't listen to it.", + "\n[Gargoyle] Flag pif_2 hasn't been initiated: can't listen to it.\n\n" + ) + ) + + # II.C Provoke error due to uninitialized event - mismatch from session arg + # both events exist but the session is wrong -> throws error + expect_error( + on( + c("pif_01", "pif_02"), + { + cat(1 + 1) + }, + session = getDefaultReactiveDomain() + ) + ) + # both events exist but the session is wrong -> throws correct error message + # a double mismatch error message as the session is wrong + out_error <- try( + on( + c("pif_01", "pif_02"), + { + cat(1 + 1) + }, + session = getDefaultReactiveDomain() + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + paste0( + "Error : [Gargoyle] Flag pif_01 hasn't been initiated: can't listen to it.", + "\n[Gargoyle] Flag pif_02 hasn't been initiated: can't listen to it.\n\n" + ) + ) + # a double mismatch error message as the session is wrong + out_error <- try( + on( + c("pif_1", "pif_02"), + { + cat(1 + 1) + }, + session = getDefaultReactiveDomain() + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + paste0( + "Error : [Gargoyle] Flag pif_1 hasn't been initiated: can't listen to it.", + "\n[Gargoyle] Flag pif_02 hasn't been initiated: can't listen to it.\n\n" + ) + ) + # a double mismatch error message as the session is wrong + out_error <- try( + on( + c("pif_01", "pif_2"), + { + cat(1 + 1) + }, + session = getDefaultReactiveDomain() + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + paste0( + "Error : [Gargoyle] Flag pif_01 hasn't been initiated: can't listen to it.", + "\n[Gargoyle] Flag pif_2 hasn't been initiated: can't listen to it.\n\n" + ) + ) + # a double mismatch error message as the session is wrong + out_error <- try( + on( + c("pif_1", "pif_2"), + { + cat(1 + 1) + }, + session = getDefaultReactiveDomain() + ), + silent = TRUE + ) + expect_equal( + out_error[[1]], + paste0( + "Error : [Gargoyle] Flag pif_1 hasn't been initiated: can't listen to it.", + "\n[Gargoyle] Flag pif_2 hasn't been initiated: can't listen to it.\n\n" + ) + ) + + # III. Test classes for double on() + expect_identical( + class( + on( + c("pif_01", "pif_02"), + { + cat(1 + 1) + }, + session = s + ) + ), + c("Observer.event", "Observer", "R6") + ) + shiny::reactiveConsole(FALSE) +})