Skip to content

shinytest2 is the successor to shinytest. shinytest was implemented using {webdriver} which uses PhantomJS. PhantomJS has been unsupported since 2017 and does not support displaying bslib’s Bootstrap v5. shinytest2 uses chromote to connect to your locally installed Chrome or Chromium application, allowing shinytest2 to display bslib’s Bootstrap v5.

To auto-migrate your existing shinytest tests to shinytest2, call shinytest2::migrate_from_shinytest(PATH_TO_APP).

Code structure

In the sub sections below, ShinyDriver is a reference to the shinytest::ShinyDriver R6 object. In each code block, the ShinyDriver object will be referred to as old. Similarly, AppDriver is a reference to shinytest2::AppDriver R6 object. shinytest2::AppDriver has replaced shinytest’s ShinyDriver object and will be referred to as new in each code block.

Methods

ShinyDriver$click()

Signature: ShinyDriver$click(name, iotype = c("auto", "input", "output"))

The parameters have been separated to be clearer in their intended use to AppDriver$click(input, output, ..., selector).

## {shinytest}
old$click("mybutton")
old$click("clickcount", iotype = "output")
old$click("my_id", iotype = "auto")

## {shinytest2}
new$click("mybutton"); new$click(input = "mybutton")
new$click(output = "clickcount")
new$click(selector = "#my_id")

ShinyDriver$executeScript()

Signature: ShinyDriver$executeScript(script, ...)

The ... must now be inserted into the script. You can do this with commands like glue::glue() or paste(). Extra parameters include file, which can contain your script content, and timeout, which will throw an error if the timeout (in milliseconds) is reached before the script returns. If a promise is the last value in the script then the resolved value will be sent back to the R session.

## {shinytest}
life <- 42
old$executeScript("1 + 1;")
old$executeScript("var life = arguments[0]; life;", life)

## {shinytest2}
life <- 42
new$get_js("1 + 1;")
new$get_js(paste0("let life = ", jsonlite::toJSON(life, auto_unbox = TRUE), "; life;"))
# If a promise is returned, the resolved value will be sent back to the R session
new$get_js(
  paset0("
  let life = ", jsonlite::toJSON(life, auto_unbox = TRUE), ";
  new Promise((resolve, reject) => resolve(life));
  ")
)

ShinyDriver$executeScriptAsync()

Signature: ShinyDriver$executeScriptAsync(script, ...)

ShinyDriver$executeScriptAsync() magically made a callback argument available in the JavaScript code and would block the R session until callback was called within script. In shinytest2 your script will need to be updated to use a JavaScript Promise. The Promise’s resolve method should be the callback that you used previously. You should also provide a maximum timeout (in milliseconds) that you are willing to wait before an error is thrown. For example:

## {shinytest}
# Wait until a button is clicked
old$executeScriptAsync(
  '
  var selector = arguments[0];
  var callback = arguments[1];
  $( selector ).one( "click", function() {
    callback();
  });
  ',
  "#mybutton"
)

## {shinytest2}
# Wait until a button is clicked
new$get_js(
  paste0("
  let selector = ", jsonlite::toJSON("#mybutton", auto_unbox = TRUE), ";
  new Promise((resolve, reject) => {
    $( selector ).one( \"click\", function() {
      resolve();
    });
  });
  "),
  timeout = 15 * 1000
)

ShinyDriver$getAllValues()

Signature: ShinyDriver$getAllValues(input = TRUE, output = TRUE, export = TRUE)

This method has been renamed to AppDriver$get_values(..., input, output, export, hash_images = TRUE). The new method has a slightly different behavior if only some of the input, output or export arguments are provided. If input, output or export are provided, then the method will return the values for the provided input, output or export names only. If no input, output or export are provided, then the method will return all values, similar to setting ShinyDriver$getAllValues(input = TRUE, output = TRUE, export = TRUE).

## {shinytest}
old$getAllValues()
# All input values
old$getAllValues(input = TRUE, output = FALSE, export = FALSE)
# Only `clickcount` output value
old$getAllValues(input = FALSE, output = "clickcount", export = FALSE)

## {shinytest2}
new$get_values()
# All input values
new$get_values(input = TRUE)
# Only `clickcount` output value
new$get_values(output = "clickcount")

ShinyDriver$getValue()

Signature: ShinyDriver$getValue(name, iotype = c("auto", "input", "output"))

This method has been renamed to AppDriver$get_value(..., input, output, export) and its parameters have been spread out into input and output, while adding support for export.

## {shinytest}
old$getValue("myinput")

## {shinytest2}
new$get_value(input = "myinput")
# Equivalent to
new$get_values(input = "myinput")$input$myinput

ShinyDriver$getUrl()

Signature: ShinyDriver$getUrl()

This method has been renamed to AppDriver$get_url().

## {shinytest}
old$getUrl()

## {shinytest2}
new$get_url()

ShinyDriver$setInputs()

Signature: ShinyDriver$setInputs(..., wait_ = TRUE, values_ = TRUE, timeout_ = 3 * 1000, allowInputNoBinding_ = FALSE, priority_ = c("input", "event"))

The method name has been renamed to AppDriver$get_values() and the parameters have been converted to snake_case. value_ support has been removed, but the same functionality can be achieved via a call to AppDriver$get_values() after a call to AppDriver$set_inputs().

## {shinytest}
getValuesResult <- old$setInputs(life = 42)

## {shinytest2}
new$set_inputs(life = 42)
get_values_result <- new$get_values()

ShinyDriver$getWindowSize(), ShinyDriver$setWindowSize()

Signatures: * ShinyDriver$getWindowSize() * ShinyDriver$setWindowSize(width, height)

These methods have been renamed to AppDriver$get_window_size() and AppDriver$set_window_size(width, height) respectively.

## {shinytest}
old$getWindowSize()
old$setWindowSize(width = 1024, height = 768)

## {shinytest2}
new$get_window_size()
new$set_window_size(width = 1024, height = 768)

ShinyDriver$checkUniqueWidgetNames()

Signature: ShinyDriver$checkUniqueWidgetNames()

This method checks to make sure all input and output names are unique. It is still run at initialization via AppDriver$new(check_names = TRUE), and if any issues are found during startup, only warnings will be displayed. The exported method has been upgraded to an expectation version and renamed to AppDriver$expect_unique_names().

## {shinytest}
old$checkUniqueWidgetNames()

## {shinytest2}
new$expect_unique_names()

Snapshots

Snapshots are now handled by testthat. To leverage this capability, use the AppDriver$expect_*() methods to assert that the value is consistent over many testing executions.

ShinyDriver$snapshotInit()

Signature: ShinyDriver$snapshotInit(path, screenshot = TRUE)

This method has been moved to the parameters: AppDriver$new(name = path). Screenshots via testthat snapshots are not allowed until an AppDriver$new(variant=) value is provided. variant is similar to the suffix value in shinytest::testApp(suffix=).

## {shinytest}
old$snapshotInit("mytest")

## {shinytest2}
new <- AppDriver$new(name = "mytest", variant = NULL)
# Suggested
new <- AppDriver$new(name = "mytest", variant = platform_variant())

ShinyDriver$snapshot()

Signature: ShinyDriver$snapshot(items = NULL, filename = NULL, screenshot = NULL)

In shinytest this method would expect a screenshot and expect all values to be consistent. This method no longer exists in shinytest2, and has been broken up into two methods that must be called independently: AppDriver$expect_screenshot() and AppDriver$expect_values(). AppDriver$expect_values() will (by default) take a debug screenshot that will never fail in an expectation. This allows for a historical record (version control) of what the app looked like while not having to constantly battle with false-positive screenshot failures. If a single output value is supplied to AppDriver$expect_values(), then the debug screenshot is zoomed in on the output value.

## {shinytest}
old$snapshot()
old$snapshot(items = list(output = "clickcount"))

## {shinytest2}
# Must supply `variant=` to be able to call `AppDriver$expect_screenshot()`, even if it is `NULL`
new <- AppDriver$new(path_to_app, variant = NULL)
new$expect_screenshot(); new$expect_values()
new$expect_screenshot(); new$expect_values(output = "clickcount")

# Suggested;
new <- AppDriver$new(path_to_app)
new$expect_values()

ShinyDriver$takeScreenshot()

Signature: ShinyDriver$takeScreenshot(file = NULL, id = NULL, parent = FALSE)

This method has been renamed to AppDriver$get_screenshot(file), and the id and parent parameters have been removed. To use a selector, set AppDriver$get_screenshot(myfile, selector = ".custom-selector") or screenshot_args directly.

## {shinytest}
old$takeScreenshot("myfile1.png")
old$takeScreenshot("myfile2.png", id = "myid")

## {shinytest2}
new$get_screenshot("myfile1.png")
new$get_screenshot("myfile2.png", selector = "#myid")
new$get_screenshot("myfile2.png", screenshot_args = list(selector = "#myid"))

ShinyDriver$snapshotDownload()

Signature: ShinyDriver$snapshotDownload(id, filename)

This method has been renamed to AppDriver$expect_download(id, filename).

## {shinytest}
old$snapshotDownload("mylinkid")

## {shinytest2}
new$expect_download("mylinkid")

ShinyDriver$stop()

Signature: ShinyDriver$stop()

This method stayed the same! 🥳 In shinytest2, this will also stop your [ChromeSession] instance and clean up any temporary Shiny log files.

ShinyDriver$uploadFile()

Signature: ShinyDriver$uploadFile(..., wait_ = TRUE, values_ = TRUE, timeout_ = 3 * 1000)

This method has been renamed to AppDriver$upload_file(...), and similar to AppDriver$set_inputs(), support for ShinyDriver$uploadFile(values_ =) has been removed.

## {shinytest}
old$uploadFile(myFileInput = "myfile.txt")

## {shinytest2}
new$upload_file(myFileInput = "myfile.txt")

ShinyDriver$waitFor()

Signature: ShinyDriver$waitFor(expr, checkInterval = 100, timeout = 3000)

This method has been renamed and had its parameters reordered to AppDriver$wait_for_js(script, timeout, interval). Like other JavaScript methods in shinytest2, script needs to explicitly return a value.

## {shinytest}
old$waitFor("$('#myid').length > 0")

## {shinytest2}
new$wait_for_js("$('#myid').length > 0");

ShinyDriver$waitForShiny()

Signature: ShinyDriver$waitForShiny()

This method has been upgraded to AppDriver$wait_for_idle(duration = 500, timeout = 30 * 1000). The ShinyDriver$waitForShiny() method only waited until a single instance in time stated that Shiny was no longer busy. The new method will wait until Shiny has been “idle” for a continuous stretch of time. It is useful to wait until Shiny has been idle for a set duration in order to avoid situations where dynamic UI needs to calculate new outputs given new input information.

## {shinytest}
old$waitForShiny()

## {shinytest2}
# Equivalent
new$wait_for_idle(duration = 0, timeout = 3 * 1000)
# Suggested (Shiny must become continuously idle for at least 500ms within 30s
new$wait_for_idle()

ShinyDriver$waitForValue()

Signature: ShinyDriver$waitForValue(name, ignore = list(NULL, ""), iotype = c("input", "output", "export"), timeout = 10000, checkInterval = 400): This (underutilized) method has had the name/iotype spread out into separate input, output, export parameters. Only one input/output/export value may be supplied in an AppDriver object. The timeout has been increased to 15 seconds. Both methods still poll the Shiny application at a set interval for the corresponding value to be something not in the ignored set of values.

## {shinytest}
old$waitForValue("myslider")
old$waitForValue("mydynamicplot", iotype = "output")

## {shinytest2}
new$wait_for_value("myslider"); new$wait_for_value(input = "myslider")
new$wait_for_value(output = "mydynamicplot")

Elements / Widgets

Direct element or Widget support for shinytest2 has been drastically reduced. With the ability to execute any JavaScript function via $get_js(script) and $run_js(script), it is now possible to reproduce many of the methods that were provided in shinytest.

ShinyDriver$findElement(), ShinyDriver$findElements(), ShinyDriver$findWidget()

Signatures: * ShinyDriver$findElement(css = NULL, linkText = NULL, partialLinkText = NULL, xpath = NULL) * ShinyDriver$findElements(css = NULL, linkText = NULL, partialLinkText = NULL, xpath = NULL) * ShinyDriver$findWidget(name, iotype = c("auto", "input", "output"))

These methods have been removed. It is suggested to use JavaScript code directly or use newer helper methods.

## {shinytest}
old$findElement("#mybutton")$click()
old$findElement("#mybutton")$getText()
old$findElement("#mybutton")$getCssValue("color")

## {shinytest2}
new$get_text(selector = "#mybutton")
new$click(selector = "#mybutton")
# No direct equivalent method. Using JavaScript instead
new$get_js('$("#mybutton").css("color")')

ShinyDriver$getSource(), ShinyDriver$getTitle()

Signatures: * ShinyDriver$getSource() * ShinyDriver$getTitle()

These methods have been removed. It is suggested to use JavaScript code directly, or to use newer helper methods.

## {shinytest}
old$getSource()
old$getTitle()

## {shinytest2}
new$get_html("html", outer_html = TRUE)
new$get_js("window.document.title;")

Testing setup

shinytest2 is heavily integrated with the testthat testing framework. Similar to shinytest, snapshots are recorded, but they are recorded via testthat snapshots. For more information on the robustness of different testing approaches, please see the Robust testing vignette.

  • ShinyDriver$new(suffix=): Please use AppDriver$new(variant=).
## {shinytest}
old <- ShinyDriver$new(path_to_app, suffix = "macos-4.1")

## {shinytest2}
new <- AppDriver$new(path_to_app, variant = "macos-4.1")

ShinyDriver$getRelativePathToApp(), ShinyDriver$getTestsDir()

Signatures: * ShinyDriver$getRelativePathToApp() * ShinyDriver$getTestsDir()

These methods have been removed as they are no longer needed given execution shinytest2 testing is always done within the testthat testing framework.

ShinyDriver$getSnapshotDir()

Signature: ShinyDriver$getSnapshotDir()

This method has been removed as testthat uses the ./_snaps directory to store snapshot outputs.

ShinyDriver$expectUpdate()

Signature: ShinyDriver$expectUpdate(output, ..., timeout = 3000, iotype = c("auto", "input", "output"))

This method is no longer supported. While knowing that an output value has been updated, it is very uncertain as to what the new value is. While possibly useful, it is not robust and therefore not recommended. Other testing methods should be explored.

Equivalent code has been provided below for legacy support.

## {shinytest}
old$expectUpdate("myoutput", myinput = 42)

## {shinytest2} (equivalent code)
myoutput_prior <- new$get_values(output = "myoutput")
new$set_inputs(myinput = 42)
testthat::expect_failure(
  testthat::expect_equal(
    new$get_values(output = "myoutput"),
    myoutput_prior
  )
)

Debugging

Debugging in shinytest2 has been unified and is enabled at all times. The shinytest debugging methods have been removed in favor of AppDriver$get_logs(), which returns a similarly shaped tibble as ShinyDriver$getDebugLog()’s data.frame. The columns have been altered to be more generic, where type has been broken into two columns: location and level.

  • ShinyDriver$getDebugLog(), ShinyDriver$getEventLog(): Now use AppDriver$get_logs().
  • ShinyDriver$logEvent(event, ...): Messages can still be recorded using AppDriver$log_message(text), but ... values are no longer supported.
  • ShinyDriver$enableDebugLogMessages(enable = TRUE): No longer used. All messages are always recorded.
## {shinytest}
old$getDebugLog(); old$getEventLog()
old$logEvent("Creating report")

## {shinytest2}
new$get_logs()
old$log_message("Creating report")

If options = list(shiny.trace = TRUE) is set when initializing an AppDriver object, then all WebSocket traffic will be recorded.

## {shinytest2}
# Record all websocket traffic. Caution!! This is very verbose!!
new <- AppDriver$new(path_to_app, options = list(shiny.trace = TRUE))
new$get_logs()

Other removed methods

  • ShinyDriver$setValue(name, value, iotype): To set an output value, it must be performed by setting an input value and having your render methods set the output value. To set an input value, use AppDriver$set_inputs().
## {shinytest}
old$setValue("myinput", 42)

## {shinytest2}
new$set_inputs(myinput = 42)
  • ShinyDriver$listWidgets(): This method has been removed and can be achieved by getting the names of the existing values: AppDriver$get_values().
## {shinytest}
old$listWidgets()

## {shinytest2}
lapply(new$get_values(), names)
  • ShinyDriver$clone(): AppDriver does not support cloning. (The underlying ChromoteSession does not support cloning, so AppDriver cannot support cloning.)

  • ShinyDriver$goBack(), ShinyDriver$refresh(): These methods have been removed, but can be achieved with AppDriver$run_js(script).

## {shinytest}
old$goBack()
old$refresh()

## {shinytest2}
# Go back
new$run_js("window.history.back();")
# Refresh page
new$run_js("window.location.reload();")
  • ShinyDriver$getAppDir(), ShinyDriver$getAppFilename(), ShinyDriver$isRmd(): AppDriver$get_dir() is the only method still supported given AppDriver$new(app_dir=) is a single directory path. The other methods have been removed as their functionality does not make sense within shinytest2.
## {shinytest}
old$isRmd()
old$getAppDir()
old$getAppFilename()

## {shinytest2}
app_dir <- new$get_dir()
rmd_files <- fs::dir_ls(new$get_dir(), regexp = "\\.[Rr]md$")
is_rmd <- length(rmd_files) >= 1
app_filename <- if (has_rmd) fs::path_file(rmd_files[1]) else NULL
  • ShinyDriver$sendKeys(name, keys): This method has been removed and there is currently no easy alternative. If you are familiar with the key code values, you can trigger them with jQuery and AppDriver$run_js(script).
## {shinytest}
old$sendKeys("myinput", webdriver::keys$enter)

## {shinytest2}
new$run_js("$('#myinput').trigger({type: 'keypress', which: 13, keyCode: 13});")