Testing of shiny modules containing other modules

Viewed 293

In a large Shiny App, I have a lot of modules within other modules. These nested modules also sometimes have input controls, e.g. textInput() or actionButton, which trigger certain events also in the parent module.

The following MWE shows the problem. The module summaryServer prints a summary of a value, but waits for the reactive from rangeServer, which is triggered by a button. I want a Testing specific for summaryServer with testServer() function from Shiny, but how can I "click" the Button in the contained rangeServer module to continue? Is that something about the Mock Shiny Session?

### TESTING ###
x <- reactiveVal(1:10)
testServer(summaryServer, args = list(var = x), {

  cat("var active?", d_act(),"\n")

  # -----------------------------
  # How to click "go" here?
  # -----------------------------

  
  cat("var active?", d_act(), "\n")
})
### The app ###
summaryUI <- function(id) {
  tagList(
    textOutput(NS(id, "min")),
    textOutput(NS(id, "mean")),
    textOutput(NS(id, "max")),
    rangeUI(NS(id, "range"))
  )
}
summaryServer <- function(id, var) {
  stopifnot(is.reactive(var))
  
  moduleServer(id, function(input, output, session) {
  
    d_act = reactiveVal("Haha nope")
    range_val = rangeServer("range", var = var)
    
    # waits to range_val
    observeEvent(range_val(),{
      d_act("TRUE")
      message(range_val())
      output$min <- renderText(range_val()[[1]])
      output$max <- renderText(range_val()[[2]])
      output$mean <- renderText(mean(var()))
    })
  })
}

rangeUI = function(id) {
  textInput(inputId = NS(id, "go"), label = "Go")
}
rangeServer = function(id, var){
  moduleServer(id, function(input, output, session){
   # when button gets clicked
    eventReactive(input$go,{
      range(var(), na.rm = TRUE)
      
    }, ignoreInit = TRUE, ignoreNULL = TRUE)
  })
}

library(shiny)

ui <- fluidPage(
  summaryUI("sum")
)

server <- function(input, output, session) {
  x = reactiveVal(1:10)
  summaryServer("sum", x)
}

# shinyApp(ui, server)
1 Answers

That is a tricky one. It works if you set both ignoreInit and ignoreNULL to FALSE but just because then you are not initially dependent on a change of go anymore, which is undesirable.

I do not think it is possible to change go inside of rangeServer when running testServer with summaryServer. You can however use {shinytest} to achieve this. Note that here you invoke and test the entire app. Therefore, when using modules, you have to call elements by their complete id, including namespaces.

(I changed go to an actionButton, everything else stays the same)

rangeUI <- function(id) {
  actionButton(inputId = NS(id, "go"),label = "Go")
}
test_that("output updates when reactive input changes", {

  # invoke app
  app <- shinytest::ShinyDriver$new("app.R")

  # initially, the button has`nt been clicked and the outputs are empty
  testthat::expect_equal(app$getValue("summary-range-go"), 0)
  testthat::expect_equal(app$getValue("summary-min"), "")

  # click the button
  app$click("summary-range-go")
  testthat::expect_equal(app$getValue("summary-range-go"), 1)
  # testthat::expect_equal(app$getValue("summary-min"), "1")

  # for some reason, the button value increased, hence is clicked,
  # but the outputs have not been triggered yet.
  # another click fixes that
  app$click("summary-range-go")
  testthat::expect_equal(app$getValue("summary-min"), "1")

})
Related