--- output: rmarkdown::html_vignette title: "Unit testing" vignette: > %\VignetteIndexEntry{Unit testing} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} available <- selenider::selenider_available() knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = available ) ``` ```{r, eval = !available, include = FALSE} message("Selenider is not available") ``` selenider is compatible with unit testing frameworks like `testthat` and `shinytest2`. In this vignette, we will explore how to write unit tests with selenider, and we will briefly describe how to automate your tests using Github Actions. ```{r setup} library(selenider) library(testthat) ``` ## Using selenider with testthat Tests contained within `testthat::test_that()` are self-contained, having no impact on other tests. selenider is no exception: selenider sessions, when created inside a `testthat::test_that()` block, will be closed automatically when the test finishes running. Remember, as always, to use the `.env` argument when wrapping `selenider_session()` in another function. `elem_expect()` also has additional features inside `testhat::test_that()`. When it succeeds, it will call `testthat::succeed()`, and when it fails, it will use `testthat::fail()` instead of throwing an error. This allows tests to continue running even if `elem_expect()` fails. ```{r error = TRUE} test_that("My test", { # session will be opened here... open_url("https://www.r-project.org/") s(".random-class") |> elem_expect(is_present) }) # and closed here! ``` ## Using selenider with shinytest2 Since shinytest2 uses chromote as a backend, it can be used with selenider. selenider can be used to add more robust UI testing to shinytest2, replacing unreliable uses of `AppDriver$expect_screenshot()`. shinytest2 does have a few UI expectations (`AppDriver$expect_text()`, `AppDriver$expect_html()` and `AppDriver$expect_js()`), but these do not include the same laziness and implicit waiting that selenider provides, making them a bit less reliable. ``` {r setup2} library(shiny) library(shinytest2) ``` Let's create a simple shiny app, consisting of a `shiny::actionButton()` and `shiny:: conditionalPanel()`. The panel is shown if the button has been clicked an odd number of times, and hidden otherwise. We would like to test that the server-side processing of the button input is done correctly, which we can do using shinytest2. However, we would also like to check that the panel is visible at the correct times, which we cannot do with shinytest2, and so we will use selenider instead. ``` {r} shiny_app <- shinyApp( ui = fluidPage( actionButton("button", label = "Click me!"), conditionalPanel( condition = "(input.button % 2) == 1", p("Button has been clicked an odd number of times.") ) |> tagAppendAttributes(id = "condpanel") ), server = function(input, output) { even <- reactive((input$button %% 2) == 0) exportTestValues(even = { even() }) } ) ``` To start a selenider session using an existing `shinytest2::AppDriver` object, supply it to the `driver` argument of `selenider_session()`: `session <- selenider_session(driver = )` ``` {r} test_that("App works", { app <- AppDriver$new(shiny_app) session <- selenider_session(driver = app) s("#condpanel") |> elem_expect(is_invisible) app$click("button") app$expect_values(export = "even") s("#condpanel") |> elem_expect(is_visible) app$click("button") app$expect_values(export = "even") s("#condpanel") |> elem_expect(is_invisible) }) ``` Note the difference in styles: while in selenider you must specify tests explicitly, shinytest2 uses a snapshot-based approach (specifying the value that you want to test and omitting the value that you expect it to be). There are advantages and disadvantages to this approach: the tests are generally easier to create and update, but a little harder to debug. If you want to use a snapshot-based style, you can do it manually, e.g.: ``` r expect_snapshot(is_visible(s("#condpanel"))) ``` However, note that the tests will no longer wait a certain period of time for the value to be correct, since the test is unaware of what the correct value is. ## Using selenider with Github Actions The complexity of using selenider with Github Actions depends on the backend that you use. If you would like to use chromote as your backend, you shouldn't need to make any special additions to your workflow files, and can safely use something like [r-lib's R CMD CHECK action](https://github.com/r-lib/actions/tree/v2-branch/examples#standard-ci-workflow). This is because chromote only requires chrome to be installed, which is already the case on Github's machines. If you want to use selenium with Github Actions, it is recommended to make use of docker. See for more information. For example, the following lines in a Github Actions yaml file will start a selenium server (version 4.15.0), supporting Firefox, on port 4444. We recommend using the "shm-size" argument to make sure you don't run out of memory. ``` yaml services: selenium: image: selenium/standalone-firefox:4.15.0-20231108 ports: - 4444:4444 options: >- --shm-size="2g" ``` This will download Firefox and start a Selenium server on port 4444. Automating a browser with Selenium consists of two parts: the server and the client. By default, `selenider_session()` tries to setup both, but we can stop this from happening by using the `options` argument. ``` r session <- selenider_session( "selenium", browser = "firefox", options = selenium_options( server_options = NULL, # Stop selenider from creating a server client_options = selenium_client_options(port = 4444L) # Use the port of the server ) ) ``` The session can then by used as usual. selenider will no longer be able to close the selenium server, but this should be done automatically in the Github Action. For more information, see how we setup our Github Actions workflow for selenium: