teal icon indicating copy to clipboard operation
teal copied to clipboard

Avoid busy wait in shinytest

Open insights-engineering-bot opened this issue 3 years ago • 0 comments

We can avoid the busy waiting until the Shiny app has started with Sys.sleep. Otherwise, tests may be unnecessarily long.

Here is the code. The app is defined as follows. It needs 10 seconds to be ready:

# to put into some directory, name this file `app.R`
shinyApp(
  ui = function() {
    div(
      textOutput("mytext")
    )
  },
  server = function(input, output, session) {
    output$mytext <- renderText({
      Sys.sleep(10)
      print(
        paste0("Sleeping", Sys.time()),
        file = file.path("~/scratch/teal", paste0(Sys.time(), ".R"))
      )
      "Hello"
    })
  }
)

Here is the code to constantly check whether the app is ready or not:

#' Wait until the Shiny app is no longer busy
#' 
#' Note that if the app runs something on the server to set some inputs 
#' on the client, this may trigger the server `reactives` again, so the 
#' Shiny app may not be ready in the proper sense. Then, you may
#' still need a manual timeout or call this function twice or more.
#' 
#' @md
#' @param app `ShinyDriver` app created through `\link[shinytest]{ShinyDriver}`
#' @param checkInterval `integer` How often to check for the condition, 
#'   in milliseconds.
#' @param timeout `integer` Timeout for the condition, in milliseconds.
#' @param warn `logical` whether to warn when the timeout is exceeded
#' @return `logical` whether the condition was `TRUE` (if timeout was reached,
#'   it may not be TRUE), also displays a warning if `FALSE`
wait_until_app_started <- function(app, checkInterval = 100, timeout = 3000, warn = TRUE) {
  stopifnot(
    is(app, "ShinyDriver"),
    is_logical_single(warn)
  )
  # runs Javascript code and returns once it is TRUE
  # we wait until the shiny application is no longer busy
  # based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 #nolint
  res <- app$waitFor(
    "!(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))",
    checkInterval = checkInterval, timeout = timeout
  )
  if (!res && warn) {
    warning("Timeout exceeded, app is still busy")
  }
  return(res)
}

library(shinytest)
app_path <- "~/scratch/dummy_wait_app" # must have file app.R
app <- ShinyDriver$new(app_path, loadTimeout = 100000, debug = "all")

# Shiny driver takes non-negligible time to start, so the app will already be busy at the first invocation
# of `wait_until_app_started`
while (!wait_until_app_started(app)) {
  print("Waiting for app to start")
}
print("App succeeded")

app$stop()

Provenance:

Creator: maximilianmordig