diff --git a/DESCRIPTION b/DESCRIPTION index 8734c972f3..a1a32c870a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -97,7 +97,6 @@ Encoding: UTF-8 Language: en-US LazyData: true Roxygen: list(markdown = TRUE, packages = c("roxy.shinylive")) -RoxygenNote: 7.3.3 Collate: 'TealAppDriver.R' 'after.R' @@ -134,3 +133,4 @@ Collate: 'validate_inputs.R' 'validations.R' 'zzz.R' +Config/roxygen2/version: 8.0.0 diff --git a/NAMESPACE b/NAMESPACE index ffb68e3e30..8489332806 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -32,6 +32,7 @@ export(modify_header) export(modify_title) export(module) export(modules) +export(need_input) export(report_card_template) export(reporter_previewer_module) export(srv_session_info) @@ -43,10 +44,12 @@ export(teal_transform_module) export(ui_session_info) export(ui_teal) export(ui_transform_teal_data) +export(use_validate_input_js) export(validate_has_data) export(validate_has_elements) export(validate_has_variable) export(validate_in) +export(validate_input) export(validate_inputs) export(validate_n_levels) export(validate_no_intersection) diff --git a/NEWS.md b/NEWS.md index 2418ce1214..28132be5ac 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ * Improved print method for `teal_module` object (#1685). * Added opt-in URL-based module navigation, controlled by `options(teal.enable_deep_linking = TRUE)` (default: `FALSE`). When enabled, the active module is reflected in the URL as `?active_module=`, and navigating to such a URL or using the browser's back/forward buttons switches to the corresponding tab (#1699). * Exported new utility functions to support module decorators: `srv_transform_teal_data()`, `ui_transform_teal_data()` and `check_decorators()` (#1697). +* Added `teal.snapshot_manager.enable` option (default: `TRUE`) to control whether the Snapshot Manager panel is rendered. Setting it to `FALSE` hides the panel and skips its server logic. # teal 1.1.0 diff --git a/R/TealAppDriver.R b/R/TealAppDriver.R index 7c0e2fd794..ee35595920 100644 --- a/R/TealAppDriver.R +++ b/R/TealAppDriver.R @@ -38,16 +38,21 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. #' #' See [`shinytest2::AppDriver`] `new` method for more details on how to change it #' via options or environment variables + #' @param teal_options (`list`) named list of R options to set inside the spawned + #' Shiny process (e.g. `list(teal.enable_deep_linking = TRUE)`). Applied at session + #' start so option-gated server logic sees them. #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$new` #' #' #' @return Object of class `TealAppDriver` initialize = function(app, options = list(), + teal_options = list(), timeout = rlang::missing_arg(), load_timeout = rlang::missing_arg(), ...) { checkmate::assert_class(app, "teal_app") + checkmate::assert_list(teal_options, names = "named") # Default timeout is hardcoded to 4s in shinytest2:::resolve_timeout # New default is set with environment variables if not already set by user # envvars have the lowest priority in shinytest2 timeout resolution. @@ -59,6 +64,15 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. extra_envvar$SHINYTEST2_TIMEOUT <- "100000" } + if (length(teal_options)) { + orig_server <- app$server + opts <- teal_options + app$server <- function(input, output, session) { + do.call(base::options, opts) + orig_server(input, output, session) + } + } + suppressWarnings( withr::with_envvar( new = extra_envvar, diff --git a/R/module_snapshot_manager.R b/R/module_snapshot_manager.R index 1060774f1e..831bb9dca9 100644 --- a/R/module_snapshot_manager.R +++ b/R/module_snapshot_manager.R @@ -71,6 +71,13 @@ #' (The snapshot will be retrieved by `module_teal` in order to set initial app state in a restored app.) #' Then that snapshot, and the previous snapshot history are dumped into the `values.rds` file in ``. #' +#' @section Disabling the snapshot manager: +#' The snapshot manager can be turned off application-wide by setting the +#' `teal.snapshot_manager.enable` option to `FALSE`. When disabled, both +#' `ui_snapshot_manager_panel()` and `srv_snapshot_manager_panel()` return `NULL`, +#' so the camera icon is not rendered and no snapshot logic is attached. +#' The option defaults to `TRUE` and is set when the `teal` package is loaded. +#' #' @param id (`character(1)`) `shiny` module instance id. #' @param slices_global (`reactiveVal`) that contains a `teal_slices` object #' containing all `teal_slice`s existing in the app, both active and inactive. @@ -86,6 +93,9 @@ NULL #' @rdname module_snapshot_manager ui_snapshot_manager_panel <- function(id) { + if (!isTRUE(getOption("teal.snapshot_manager.enable", TRUE))) { + return(NULL) + } ns <- NS(id) .expand_button( id = ns("show_snapshot_manager"), @@ -96,6 +106,9 @@ ui_snapshot_manager_panel <- function(id) { #' @rdname module_snapshot_manager srv_snapshot_manager_panel <- function(id, slices_global) { + if (!isTRUE(getOption("teal.snapshot_manager.enable", TRUE))) { + return(NULL) + } moduleServer(id, function(input, output, session) { logger::log_debug("srv_snapshot_manager_panel initializing") setBookmarkExclude(c("show_snapshot_manager")) diff --git a/R/module_teal.R b/R/module_teal.R index 15fdd6d142..b7de560986 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -108,6 +108,7 @@ ui_teal <- function(id, modules) { include_teal_css_js(), shinyjs::useShinyjs(), shiny::includeScript(system.file("js/extendShinyJs.js", package = "teal")), + use_validate_input_js(), shiny_busy_message_panel, tags$div(id = ns("tabpanel_wrapper"), class = "teal-body", navbar), tags$hr(style = "margin: 1rem 0 0.5rem 0;") diff --git a/R/validate_inputs.R b/R/validate_inputs.R index dc18f4490c..2640ab237d 100644 --- a/R/validate_inputs.R +++ b/R/validate_inputs.R @@ -193,3 +193,207 @@ any_names <- function(x) { } ) } + +#' Validate input +#' +#' Validate a Shiny input and send validation messages to the client. +#' The message appears both in the input widget and in the output element +#' that calls `validate_input`. +#' @details +#' - `validate_input():` +#' +#' Validate a Shiny input and send validation messages to the client. +#' The message appears both in the input widget and in the output element +#' that calls `validate_input`. +#' +#' @param inputId (`character`) Character of input ID(s) to validate +#' @param condition (`logical(1)`, `function(x)`) Logical value or function returning logical value. +#' Condition should determine expected state, `FALSE` throws. +#' @param message (`character(1)`) Character string of validation message to display. +#' @param session Shiny session object +#' +#' @return `NULL` or `shiny.silent.error` when condition is not met +#' +#' @examples +#' # Only run examples in interactive R sessions +#' options(device.ask.default = FALSE) +#' +#' ui <- fluidPage( +#' checkboxGroupInput("in1", "Check some letters", choices = head(LETTERS)), +#' selectizeInput("in2", "Select a state", choices = c("", state.name)), +#' use_validate_input_js(), +#' plotOutput("plot") +#' ) +#' +#' server <- function(input, output) { +#' output$plot <- renderPlot({ +#' validate_input("in1", condition = function(x) length(x) > 0, "Check at least one letter!") +#' validate_input("in2", condition = function(x) x != "", "Please choose a state.") +#' plot(1:10, main = paste(c(input$in1, input$in2), collapse = ", ")) +#' }) +#' } +#' +#' if (interactive()) { +#' shinyApp(ui, server) +#' } +#' +#' my_module <- module( +#' label = "My Module", +#' datanames = NULL, +#' ui = function(id) { +#' ns <- NS(id) +#' tagList( +#' checkboxGroupInput(ns("letters"), "Select letters:", choices = head(LETTERS), inline = TRUE), +#' checkboxGroupInput(ns("letters2"), "Select letters:", choices = head(LETTERS), inline = TRUE), +#' tags$h3("Sample plot"), +#' plotOutput(ns("plot")) +#' ) +#' }, +#' server = function(id, data) { +#' moduleServer(id, function(input, output, session) { +#' output$plot <- renderPlot({ +#' validate_input( +#' "letters", +#' condition = function(x) length(x) > 0, +#' message = "Select at least one letter." +#' ) +#' validate_input( +#' c("letters", "letters2"), +#' condition = function(x, y) all(!x %in% y), +#' message = "Letters in the first group should not be in the second group." +#' ) +#' tab <- rbind( +#' Group1 = as.integer(head(LETTERS) %in% input$letters), +#' Group2 = as.integer(head(LETTERS) %in% input$letters2) +#' ) +#' colnames(tab) <- head(LETTERS) +#' barplot( +#' tab, +#' beside = TRUE, legend.text = TRUE, main = "Selected letters per group", +#' col = c("steelblue", "tomato") +#' ) +#' }) +#' }) +#' } +#' ) +#' +#' app <- init( +#' data = within(teal_data(), iris <- iris), +#' modules = my_module +#' ) +#' +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +#' @export +validate_input <- function(inputId, # nolint + condition = function(x) TRUE, + message = "Check the input", + session = shiny::getDefaultReactiveDomain()) { + validate(need_input(inputId, condition, message, session)) +} + +#' @rdname validate_input +#' @details +#' - `need_input()`: Validate a Shiny input and returns a message to be used in a [`shiny::validate()`] call. +#' The message is sent to the client and appears both in the input widget. +#' To observe the message in the output element, `need_input()` should be called inside a +#' `shiny::validate()` call, e.g. via `validate(validate_input(...))`. +#' @export +#' @examples +#' my_module <- module( +#' label = "My Module", +#' datanames = NULL, +#' ui = function(id) { +#' ns <- NS(id) +#' tagList( +#' checkboxGroupInput(ns("letters1"), "Select letters:", choices = head(LETTERS), inline = TRUE), +#' checkboxGroupInput(ns("letters2"), "Select letters:", choices = head(LETTERS), inline = TRUE), +#' tags$h3("Sample plot"), +#' plotOutput(ns("plot")) +#' ) +#' }, +#' server = function(id, data) { +#' moduleServer(id, function(input, output, session) { +#' output$plot <- renderPlot({ +#' validate( +#' need_input( +#' "letters1", +#' condition = function(x) length(x) > 0, +#' message = "Select at least one letter." +#' ), +#' need_input( +#' c("letters1", "letters2"), +#' condition = function(x, y) all(!x %in% y), +#' message = "Letters in the first group should not be in the second group." +#' ) +#' ) +#' tab <- rbind( +#' Group1 = as.integer(head(LETTERS) %in% input$letters1), +#' Group2 = as.integer(head(LETTERS) %in% input$letters2) +#' ) +#' colnames(tab) <- head(LETTERS) +#' barplot( +#' tab, +#' beside = TRUE, legend.text = TRUE, main = "Selected letters per group", +#' col = c("steelblue", "tomato") +#' ) +#' }) +#' }) +#' } +#' ) +#' +#' app <- init( +#' data = within(teal_data(), iris <- iris), +#' modules = my_module +#' ) +#' +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +need_input <- function(inputId, # nolint + condition = function(x) TRUE, + message = "Check the input", + session = shiny::getDefaultReactiveDomain()) { + # Simple fingerprint based on inputId and message to ensure validation messages + # in Javascript are not incorrectly removed. + fingerprint <- paste0(inputId, collapse = "|") + + checkmate::assert_character(inputId, min.len = 1) + checkmate::assert( + checkmate::check_flag(condition), + checkmate::check_function(condition, nargs = length(inputId)) + ) + checkmate::assert_string(message) + + # Evaluate condition if it's a function + condition_result <- if (is.function(condition)) { + input_value <- lapply(inputId, function(id) session$input[[id]]) + checkmate::assert_flag(do.call(condition, input_value)) + } else { + condition + } + + # Send custom message to JavaScript handler + lapply(inputId, function(id) { + session$sendCustomMessage("validateInput", list( + inputId = session$ns(id), + isValid = condition_result, + message = message, + fingerprint = fingerprint + )) + }) + + need(condition_result, message) +} + +#' @rdname validate_input +#' @details +#' - `use_validate_input_js()`: +#' +#' Include JavaScript for client-side input validation. +#' @export +use_validate_input_js <- function() { + addResourcePath("js", system.file("js", package = "teal")) + singleton(tags$script(src = "js/input-validator.js")) +} diff --git a/R/zzz.R b/R/zzz.R index 5dd0c5a2f5..88d10863d8 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -9,6 +9,7 @@ teal.sidebar.width = 250, teal.reporter.nav_buttons = c("preview", "download", "load", "reset"), teal.show_src = TRUE, + teal.snapshot_manager.enable = TRUE, teal.bs_theme = .default_teal_bslib_theming ) @@ -20,6 +21,9 @@ teal.logger::register_logger("teal") teal.logger::register_handlers("teal") + if (getRversion() < "4.4") { + assign("%||%", rlang::`%||%`, envir = getNamespace(pkgname)) + } invisible() } diff --git a/inst/css/validation.css b/inst/css/validation.css index 665fac2d73..ae90bf1522 100644 --- a/inst/css/validation.css +++ b/inst/css/validation.css @@ -40,8 +40,8 @@ padding: 0.5em 0 0.5em 0.5em; } -.teal_primary_col > .teal_validated:has(.teal-output-warning), -.teal_primary_col > .teal_validated:has(.shiny-output-error) { +.teal_primary_col>.teal_validated:has(.teal-output-warning), +.teal_primary_col>.teal_validated:has(.shiny-output-error) { width: 100%; background-color: rgba(223, 70, 97, 0.1); border: 1px solid red; diff --git a/inst/js/extendShinyJs.js b/inst/js/extendShinyJs.js index f92816b62b..b28a2898e1 100644 --- a/inst/js/extendShinyJs.js +++ b/inst/js/extendShinyJs.js @@ -1,7 +1,7 @@ // This file contains functions that should be executed at the start of each session, // not included in the original HTML -shinyjs.autoFocusModal = function(id) { +shinyjs.autoFocusModal = function (id) { document.getElementById('shiny-modal').addEventListener( 'shown.bs.modal', () => document.getElementById(id).focus(), @@ -9,7 +9,7 @@ shinyjs.autoFocusModal = function(id) { ); } -shinyjs.enterToSubmit = function(id, submit_id) { +shinyjs.enterToSubmit = function (id, submit_id) { document.getElementById('shiny-modal').addEventListener( 'shown.bs.modal', () => document.getElementById(id).addEventListener('keyup', (e) => { @@ -22,10 +22,9 @@ shinyjs.enterToSubmit = function(id, submit_id) { } // Custom message handler to get document title and send it to the server -Shiny.addCustomMessageHandler('teal-get-document-title', function(message) { +Shiny.addCustomMessageHandler('teal-get-document-title', function (message) { const title = document.title; const inputId = message.inputId; console.log('Teal: Sending document title:', title, 'to input:', inputId); - Shiny.setInputValue(inputId, title, {priority: 'event'}); + Shiny.setInputValue(inputId, title, { priority: 'event' }); }); - diff --git a/inst/js/input-validator.js b/inst/js/input-validator.js new file mode 100644 index 0000000000..cb9eee7bf7 --- /dev/null +++ b/inst/js/input-validator.js @@ -0,0 +1,100 @@ +$(document).on('shiny:connected', function () { + function withContainer(inputId, callback, timeoutMs) { + timeoutMs = timeoutMs || 5000; + + function findContainer() { + // First try direct match + var container = $('.shiny-input-container#' + inputId); + if (container.length === 0) { + // Then try to find container that has this input + container = $('.shiny-input-container:has(#' + inputId + ')'); + } + return container.length > 0 ? container : null; + } + + var existing = findContainer(); + if (existing) { + callback(existing); + return; + } + + // Element not yet in DOM — observe until it appears or we time out + var observer = new MutationObserver(function () { + var found = findContainer(); + if (found) { + observer.disconnect(); + clearTimeout(timer); + callback(found); + } + }); + + // More efficient approach: find a relevant parent container to observe instead of the entire body + function findTargetContainer() { + // Try to find a container with an ID that partially matches inputId + var idComponents = inputId.split('-'); + + // Look for potential parent containers by checking IDs that partially match + for (var i = idComponents.length; i >= 1; i--) { + var partialId = idComponents.slice(0, i).join('-'); + var potentialContainer = $('#' + partialId); + if (potentialContainer.length > 0) { + // Return the closest shiny container or the element itself + var shinyContainer = potentialContainer.closest('.shiny-bound-input, .shiny-input-container, .form-group'); + return shinyContainer.length > 0 ? shinyContainer[0] : potentialContainer[0]; + } + } + + // Last resort: observe the body (but this should rarely happen) + return document.body; + } + + // Observe only the relevant container instead of the entire body + var targetContainer = findTargetContainer(); + observer.observe(targetContainer, { childList: true, subtree: true }); + + var timer = setTimeout(function () { + observer.disconnect(); + console.warn('Container not found for input after timeout: ' + inputId); + }, timeoutMs); + } + + function applyValidation(container, inputId, isValid, message, fingerprint) { + // Remove existing validation messages from siblings of the container + const data_ref = `${inputId}-${fingerprint}`; + const child_selector = `.shiny-input-validation-error[data-ref="${data_ref}"]`; + container.parent().children(child_selector).remove(); + + // Add UI element for validation message if not valid + if (!isValid && message && message.trim() !== '') { + const validationSpan = $('') + .attr('data-ref', data_ref) + .addClass('shiny-output-error') + .addClass('shiny-input-validation-error') + .text(message); + container.after(validationSpan); + } + + // Clear validation message on next input change to avoid having stale messages + const found = container.find(`#${inputId}`); + const inputEl = found.length ? found : container; + let previousValue = inputEl.val(); + // Only remove if the value really changed, not just a re-render + inputEl.off('shiny:inputchanged').one('shiny:inputchanged', function (event) { + if (event.name !== inputEl.attr('id')) { + return; + } + // Only clear if the value actually changed + if (JSON.stringify(event.value) === JSON.stringify(previousValue)) { + return; + } + previousValue = event.value; + container.parent().children(child_selector).remove() // Remove validation message + }); + } + + Shiny.addCustomMessageHandler('validateInput', function (data) { + withContainer(data.inputId, function (container) { + applyValidation(container, data.inputId, data.isValid, data.message, data.fingerprint); + }); + }); +}); diff --git a/man/TealAppDriver.Rd b/man/TealAppDriver.Rd index e28d642baa..ae28654951 100644 --- a/man/TealAppDriver.Rd +++ b/man/TealAppDriver.Rd @@ -4,11 +4,6 @@ \alias{TealAppDriver} \title{Drive a \code{teal} application} \description{ -Drive a \code{teal} application - -Drive a \code{teal} application -} -\details{ Extension of the \code{shinytest2::AppDriver} class with methods for driving a teal application for performing interactions for \code{shinytest2} tests. } @@ -18,647 +13,681 @@ driving a teal application for performing interactions for \code{shinytest2} tes } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-TealAppDriver-new}{\code{TealAppDriver$new()}} -\item \href{#method-TealAppDriver-stop}{\code{TealAppDriver$stop()}} -\item \href{#method-TealAppDriver-click}{\code{TealAppDriver$click()}} -\item \href{#method-TealAppDriver-expect_no_shiny_error}{\code{TealAppDriver$expect_no_shiny_error()}} -\item \href{#method-TealAppDriver-expect_no_validation_error}{\code{TealAppDriver$expect_no_validation_error()}} -\item \href{#method-TealAppDriver-expect_validation_error}{\code{TealAppDriver$expect_validation_error()}} -\item \href{#method-TealAppDriver-set_input}{\code{TealAppDriver$set_input()}} -\item \href{#method-TealAppDriver-navigate_teal_tab}{\code{TealAppDriver$navigate_teal_tab()}} -\item \href{#method-TealAppDriver-namespaces}{\code{TealAppDriver$namespaces()}} -\item \href{#method-TealAppDriver-get_active_module_input}{\code{TealAppDriver$get_active_module_input()}} -\item \href{#method-TealAppDriver-get_active_module_output}{\code{TealAppDriver$get_active_module_output()}} -\item \href{#method-TealAppDriver-get_active_module_table_output}{\code{TealAppDriver$get_active_module_table_output()}} -\item \href{#method-TealAppDriver-get_active_module_plot_output}{\code{TealAppDriver$get_active_module_plot_output()}} -\item \href{#method-TealAppDriver-set_active_module_input}{\code{TealAppDriver$set_active_module_input()}} -\item \href{#method-TealAppDriver-get_active_filter_vars}{\code{TealAppDriver$get_active_filter_vars()}} -\item \href{#method-TealAppDriver-get_active_data_summary_table}{\code{TealAppDriver$get_active_data_summary_table()}} -\item \href{#method-TealAppDriver-is_visible}{\code{TealAppDriver$is_visible()}} -\item \href{#method-TealAppDriver-expect_visible}{\code{TealAppDriver$expect_visible()}} -\item \href{#method-TealAppDriver-expect_hidden}{\code{TealAppDriver$expect_hidden()}} -\item \href{#method-TealAppDriver-get_active_data_filters}{\code{TealAppDriver$get_active_data_filters()}} -\item \href{#method-TealAppDriver-add_filter_var}{\code{TealAppDriver$add_filter_var()}} -\item \href{#method-TealAppDriver-remove_filter_var}{\code{TealAppDriver$remove_filter_var()}} -\item \href{#method-TealAppDriver-set_active_filter_selection}{\code{TealAppDriver$set_active_filter_selection()}} -\item \href{#method-TealAppDriver-get_attr}{\code{TealAppDriver$get_attr()}} -\item \href{#method-TealAppDriver-get_html_rvest}{\code{TealAppDriver$get_html_rvest()}} -\item \href{#method-TealAppDriver-open_url}{\code{TealAppDriver$open_url()}} -\item \href{#method-TealAppDriver-wait_for_active_module_value}{\code{TealAppDriver$wait_for_active_module_value()}} -} -} -\if{html}{\out{ -
Inherited methods + \itemize{ + \item \href{#method-TealAppDriver-initialize}{\code{TealAppDriver$new()}} + \item \href{#method-TealAppDriver-stop}{\code{TealAppDriver$stop()}} + \item \href{#method-TealAppDriver-click}{\code{TealAppDriver$click()}} + \item \href{#method-TealAppDriver-expect_no_shiny_error}{\code{TealAppDriver$expect_no_shiny_error()}} + \item \href{#method-TealAppDriver-expect_no_validation_error}{\code{TealAppDriver$expect_no_validation_error()}} + \item \href{#method-TealAppDriver-expect_validation_error}{\code{TealAppDriver$expect_validation_error()}} + \item \href{#method-TealAppDriver-set_input}{\code{TealAppDriver$set_input()}} + \item \href{#method-TealAppDriver-navigate_teal_tab}{\code{TealAppDriver$navigate_teal_tab()}} + \item \href{#method-TealAppDriver-namespaces}{\code{TealAppDriver$namespaces()}} + \item \href{#method-TealAppDriver-get_active_module_input}{\code{TealAppDriver$get_active_module_input()}} + \item \href{#method-TealAppDriver-get_active_module_output}{\code{TealAppDriver$get_active_module_output()}} + \item \href{#method-TealAppDriver-get_active_module_table_output}{\code{TealAppDriver$get_active_module_table_output()}} + \item \href{#method-TealAppDriver-get_active_module_plot_output}{\code{TealAppDriver$get_active_module_plot_output()}} + \item \href{#method-TealAppDriver-set_active_module_input}{\code{TealAppDriver$set_active_module_input()}} + \item \href{#method-TealAppDriver-get_active_filter_vars}{\code{TealAppDriver$get_active_filter_vars()}} + \item \href{#method-TealAppDriver-get_active_data_summary_table}{\code{TealAppDriver$get_active_data_summary_table()}} + \item \href{#method-TealAppDriver-is_visible}{\code{TealAppDriver$is_visible()}} + \item \href{#method-TealAppDriver-expect_visible}{\code{TealAppDriver$expect_visible()}} + \item \href{#method-TealAppDriver-expect_hidden}{\code{TealAppDriver$expect_hidden()}} + \item \href{#method-TealAppDriver-get_active_data_filters}{\code{TealAppDriver$get_active_data_filters()}} + \item \href{#method-TealAppDriver-add_filter_var}{\code{TealAppDriver$add_filter_var()}} + \item \href{#method-TealAppDriver-remove_filter_var}{\code{TealAppDriver$remove_filter_var()}} + \item \href{#method-TealAppDriver-set_active_filter_selection}{\code{TealAppDriver$set_active_filter_selection()}} + \item \href{#method-TealAppDriver-get_attr}{\code{TealAppDriver$get_attr()}} + \item \href{#method-TealAppDriver-get_html_rvest}{\code{TealAppDriver$get_html_rvest()}} + \item \href{#method-TealAppDriver-open_url}{\code{TealAppDriver$open_url()}} + \item \href{#method-TealAppDriver-wait_for_active_module_value}{\code{TealAppDriver$wait_for_active_module_value()}} + } +} +\if{html}{\out{
Inherited methods -
-}} +
}} \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-TealAppDriver-new}{}}} -\subsection{Method \code{new()}}{ -Initialize a \code{TealAppDriver} object for testing a \code{teal} application. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$new( +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TealAppDriver-initialize}{}}} +\subsection{\code{TealAppDriver$new()}}{ + Initialize a \code{TealAppDriver} object for testing a \code{teal} application. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$new( app, options = list(), + teal_options = list(), timeout = rlang::missing_arg(), load_timeout = rlang::missing_arg(), ... -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{app}}{(\code{teal_app})} - -\item{\code{options}}{(\code{list}) passed to \code{shinyApp(options)}. See \code{\link[shiny:shinyApp]{shiny::shinyApp()}}.} - -\item{\code{timeout}}{(\code{numeric}) Default number of milliseconds for any timeout or +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{app}}{(\code{teal_app})} + \item{\code{options}}{(\code{list}) passed to \code{shinyApp(options)}. See \code{\link[shiny:shinyApp]{shiny::shinyApp()}}.} + \item{\code{teal_options}}{(\code{list}) named list of R options to set inside the spawned +Shiny process (e.g. \code{list(teal.enable_deep_linking = TRUE)}). Applied at session +start so option-gated server logic sees them.} + \item{\code{timeout}}{(\code{numeric}) Default number of milliseconds for any timeout or timeout_ parameter in the \code{TealAppDriver} class. Defaults to 20s. See \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{new} method for more details on how to change it via options or environment variables.} - -\item{\code{load_timeout}}{(\code{numeric}) How long to wait for the app to load, in ms. + \item{\code{load_timeout}}{(\code{numeric}) How long to wait for the app to load, in ms. This includes the time to start R. Defaults to 100s. See \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{new} method for more details on how to change it via options or environment variables} - -\item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$new}} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -Object of class \code{TealAppDriver} -} + \item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$new}} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + Object of class \code{TealAppDriver} + } } + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-stop}{}}} -\subsection{Method \code{stop()}}{ -Extension of the parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{stop} method that prints the logs +\subsection{\code{TealAppDriver$stop()}}{ + Extension of the parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{stop} method that prints the logs if the \code{ACTIONS_STEP_DEBUG} environment variable is set to \code{true} (case of value is ignored). -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$stop(...)}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$stop(...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{...}}{arguments passed to parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{click()} method.} + } + \if{html}{\out{
}} + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{...}}{arguments passed to parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{click()} method.} -} -\if{html}{\out{
}} -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-click}{}}} -\subsection{Method \code{click()}}{ -Append parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{click} method with a call to \code{waif_for_idle()} method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$click(...)}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$click()}}{ + Append parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{click} method with a call to \code{waif_for_idle()} method. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$click(...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{...}}{arguments passed to parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{click()} method.} + } + \if{html}{\out{
}} + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{...}}{arguments passed to parent \code{\link[shinytest2:AppDriver]{shinytest2::AppDriver}} \code{click()} method.} -} -\if{html}{\out{
}} -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-expect_no_shiny_error}{}}} -\subsection{Method \code{expect_no_shiny_error()}}{ -Check if the app has shiny errors. This checks for global shiny errors. +\subsection{\code{TealAppDriver$expect_no_shiny_error()}}{ + Check if the app has shiny errors. This checks for global shiny errors. Note that any shiny errors dependent on shiny server render will only be captured after the teal module tab is visited because shiny will not trigger server computations when the tab is invisible. So, navigate to the module tab you want to test before calling this function. Although, this catches errors hidden in the other module tabs if they are already rendered. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$expect_no_shiny_error()}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$expect_no_shiny_error()} + \if{html}{\out{
}} + } } -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-expect_no_validation_error}{}}} -\subsection{Method \code{expect_no_validation_error()}}{ -Check if the app has no validation errors. This checks for global shiny validation errors. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$expect_no_validation_error()}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$expect_no_validation_error()}}{ + Check if the app has no validation errors. This checks for global shiny validation errors. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$expect_no_validation_error()} + \if{html}{\out{
}} + } } -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-expect_validation_error}{}}} -\subsection{Method \code{expect_validation_error()}}{ -Check if the app has validation errors. This checks for global shiny validation errors. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$expect_validation_error()}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$expect_validation_error()}}{ + Check if the app has validation errors. This checks for global shiny validation errors. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$expect_validation_error()} + \if{html}{\out{
}} + } } -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-set_input}{}}} -\subsection{Method \code{set_input()}}{ -Set the input in the \code{teal} app. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$set_input(input_id, value, ...)}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$set_input()}}{ + Set the input in the \code{teal} app. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$set_input(input_id, value, ...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{input_id}}{(character) The shiny input id with it's complete name space.} + \item{\code{value}}{The value to set the input to.} + \item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{TealAppDriver} object invisibly. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{input_id}}{(character) The shiny input id with it's complete name space.} - -\item{\code{value}}{The value to set the input to.} - -\item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{TealAppDriver} object invisibly. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-navigate_teal_tab}{}}} -\subsection{Method \code{navigate_teal_tab()}}{ -Navigate the teal tabs in the \code{teal} app. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$navigate_teal_tab(tab)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{tab}}{(character) Labels of tabs to navigate to. +\subsection{\code{TealAppDriver$navigate_teal_tab()}}{ + Navigate the teal tabs in the \code{teal} app. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$navigate_teal_tab(tab)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{tab}}{(character) Labels of tabs to navigate to. Note: Make sure to provide unique labels for the tabs.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{TealAppDriver} object invisibly. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{TealAppDriver} object invisibly. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-namespaces}{}}} -\subsection{Method \code{namespaces()}}{ -\code{NS} in different sections of \code{teal} app -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$namespaces(is_selector = FALSE)}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$namespaces()}}{ + \code{NS} in different sections of \code{teal} app + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$namespaces(is_selector = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{is_selector}}{(\code{logical(1)}) whether \code{ns} function should prefix with \verb{#}.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + list of \code{ns}. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{is_selector}}{(\code{logical(1)}) whether \code{ns} function should prefix with \verb{#}.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -list of \code{ns}. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_module_input}{}}} -\subsection{Method \code{get_active_module_input()}}{ -Get the input from the module in the \code{teal} app. +\subsection{\code{TealAppDriver$get_active_module_input()}}{ + Get the input from the module in the \code{teal} app. This function will only access inputs from the name space of the current active teal module. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_module_input(input_id)}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_module_input(input_id)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{input_id}}{(character) The shiny input id to get the value from.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The value of the shiny input. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{input_id}}{(character) The shiny input id to get the value from.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The value of the shiny input. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_module_output}{}}} -\subsection{Method \code{get_active_module_output()}}{ -Get the output from the module in the \code{teal} app. +\subsection{\code{TealAppDriver$get_active_module_output()}}{ + Get the output from the module in the \code{teal} app. This function will only access outputs from the name space of the current active teal module. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_module_output(output_id)}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_module_output(output_id)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{output_id}}{(character) The shiny output id to get the value from.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The value of the shiny output. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{output_id}}{(character) The shiny output id to get the value from.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The value of the shiny output. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_module_table_output}{}}} -\subsection{Method \code{get_active_module_table_output()}}{ -Get the output from the module's \code{teal.widgets::table_with_settings} or \code{DT::DTOutput} in the \code{teal} app. +\subsection{\code{TealAppDriver$get_active_module_table_output()}}{ + Get the output from the module's \code{teal.widgets::table_with_settings} or \code{DT::DTOutput} in the \code{teal} app. This function will only access outputs from the name space of the current active teal module. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_module_table_output(table_id, which = 1)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{table_id}}{(\code{character(1)}) The id of the table in the active teal module's name space.} - -\item{\code{which}}{(integer) If there is more than one table, which should be extracted. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_module_table_output(table_id, which = 1)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{table_id}}{(\code{character(1)}) The id of the table in the active teal module's name space.} + \item{\code{which}}{(integer) If there is more than one table, which should be extracted. By default it will look for a table that is built using \code{teal.widgets::table_with_settings}.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The data.frame with table contents. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -The data.frame with table contents. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_module_plot_output}{}}} -\subsection{Method \code{get_active_module_plot_output()}}{ -Get the output from the module's \code{teal.widgets::plot_with_settings} in the \code{teal} app. +\subsection{\code{TealAppDriver$get_active_module_plot_output()}}{ + Get the output from the module's \code{teal.widgets::plot_with_settings} in the \code{teal} app. This function will only access plots from the name space of the current active teal module. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_module_plot_output(plot_id)}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_module_plot_output(plot_id)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{plot_id}}{(\code{character(1)}) The id of the plot in the active teal module's name space.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{src} attribute as \code{character(1)} vector. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{plot_id}}{(\code{character(1)}) The id of the plot in the active teal module's name space.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{src} attribute as \code{character(1)} vector. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-set_active_module_input}{}}} -\subsection{Method \code{set_active_module_input()}}{ -Set the input in the module in the \code{teal} app. +\subsection{\code{TealAppDriver$set_active_module_input()}}{ + Set the input in the module in the \code{teal} app. This function will only set inputs in the name space of the current active teal module. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$set_active_module_input(input_id, value, ...)}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$set_active_module_input(input_id, value, ...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{input_id}}{(character) The shiny input id to get the value from.} + \item{\code{value}}{The value to set the input to.} + \item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{TealAppDriver} object invisibly. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{input_id}}{(character) The shiny input id to get the value from.} - -\item{\code{value}}{The value to set the input to.} - -\item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{TealAppDriver} object invisibly. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_filter_vars}{}}} -\subsection{Method \code{get_active_filter_vars()}}{ -Get the active datasets that can be accessed via the filter panel of the current active teal module. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_filter_vars()}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$get_active_filter_vars()}}{ + Get the active datasets that can be accessed via the filter panel of the current active teal module. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_filter_vars()} + \if{html}{\out{
}} + } } -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_data_summary_table}{}}} -\subsection{Method \code{get_active_data_summary_table()}}{ -Get the active data summary table -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_data_summary_table()}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$get_active_data_summary_table()}}{ + Get the active data summary table + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_data_summary_table()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + \code{data.frame} + } } -\subsection{Returns}{ -\code{data.frame} -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-is_visible}{}}} -\subsection{Method \code{is_visible()}}{ -Test if \code{DOM} elements are visible on the page with a JavaScript call. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$is_visible( +\subsection{\code{TealAppDriver$is_visible()}}{ + Test if \code{DOM} elements are visible on the page with a JavaScript call. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$is_visible( selector, content_visibility_auto = FALSE, opacity_property = FALSE, visibility_property = FALSE -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{selector}}{(\code{character(1)}) \code{CSS} selector to check visibility. +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{selector}}{(\code{character(1)}) \code{CSS} selector to check visibility. A \code{CSS} id will return only one element if the UI is well formed.} - -\item{\code{content_visibility_auto, opacity_property, visibility_property}}{(\code{logical(1)}) See more information + \item{\code{content_visibility_auto, opacity_property, visibility_property}}{(\code{logical(1)}) See more information on \url{https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility}.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + Logical vector with all occurrences of the selector. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -Logical vector with all occurrences of the selector. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-expect_visible}{}}} -\subsection{Method \code{expect_visible()}}{ -Expect that \code{DOM} elements are visible on the page with a JavaScript call. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$expect_visible( +\subsection{\code{TealAppDriver$expect_visible()}}{ + Expect that \code{DOM} elements are visible on the page with a JavaScript call. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$expect_visible( selector, content_visibility_auto = FALSE, opacity_property = FALSE, visibility_property = FALSE, timeout -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{selector}}{(\code{character(1)}) \code{CSS} selector to check visibility. +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{selector}}{(\code{character(1)}) \code{CSS} selector to check visibility. if more than one element is found, at least one must be visible for this expectation to be successful.} - -\item{\code{content_visibility_auto, opacity_property, visibility_property}}{(\code{logical(1)}) + \item{\code{content_visibility_auto, opacity_property, visibility_property}}{(\code{logical(1)}) See more information on \url{https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility}.} - -\item{\code{timeout}}{(\code{numeric(1)}) Time in milliseconds to wait for the expectation to be met. + \item{\code{timeout}}{(\code{numeric(1)}) Time in milliseconds to wait for the expectation to be met. Defaults to the \code{timeout} parameter set during initialization of the \code{TealAppDriver} object.} + } + \if{html}{\out{
}} + } } -\if{html}{\out{
}} -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-expect_hidden}{}}} -\subsection{Method \code{expect_hidden()}}{ -Expect that \code{DOM} elements are hidden on the page with a JavaScript call. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$expect_hidden( +\subsection{\code{TealAppDriver$expect_hidden()}}{ + Expect that \code{DOM} elements are hidden on the page with a JavaScript call. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$expect_hidden( selector, content_visibility_auto = FALSE, opacity_property = FALSE, visibility_property = FALSE, timeout -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{selector}}{(\code{character(1)}) \code{CSS} selector to check visibility. +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{selector}}{(\code{character(1)}) \code{CSS} selector to check visibility. if more than one element is found, all of them must be invisible for this expectation to be successful.} - -\item{\code{content_visibility_auto, opacity_property, visibility_property}}{(\code{logical(1)}) + \item{\code{content_visibility_auto, opacity_property, visibility_property}}{(\code{logical(1)}) See more information on \url{https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility}.} - -\item{\code{timeout}}{(\code{numeric(1)}) Time in milliseconds to wait for the expectation to be met. + \item{\code{timeout}}{(\code{numeric(1)}) Time in milliseconds to wait for the expectation to be met. Defaults to the \code{timeout} parameter set during initialization of the \code{TealAppDriver} object.} + } + \if{html}{\out{
}} + } } -\if{html}{\out{
}} -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_active_data_filters}{}}} -\subsection{Method \code{get_active_data_filters()}}{ -Get the active filter variables from a dataset in the \code{teal} app. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_active_data_filters(dataset_name = NULL)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{dataset_name}}{(character) The name of the dataset to get the filter variables from. +\subsection{\code{TealAppDriver$get_active_data_filters()}}{ + Get the active filter variables from a dataset in the \code{teal} app. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_active_data_filters(dataset_name = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{dataset_name}}{(character) The name of the dataset to get the filter variables from. If \code{NULL}, the filter variables for all the datasets will be returned in a list.} + } + \if{html}{\out{
}} + } } -\if{html}{\out{
}} -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-add_filter_var}{}}} -\subsection{Method \code{add_filter_var()}}{ -Add a new variable from the dataset to be filtered. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$add_filter_var(dataset_name, var_name, ...)}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$add_filter_var()}}{ + Add a new variable from the dataset to be filtered. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$add_filter_var(dataset_name, var_name, ...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{dataset_name}}{(character) The name of the dataset to add the filter variable to.} + \item{\code{var_name}}{(character) The name of the variable to add to the filter panel.} + \item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{TealAppDriver} object invisibly. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{dataset_name}}{(character) The name of the dataset to add the filter variable to.} - -\item{\code{var_name}}{(character) The name of the variable to add to the filter panel.} - -\item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{TealAppDriver} object invisibly. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-remove_filter_var}{}}} -\subsection{Method \code{remove_filter_var()}}{ -Remove an active filter variable of a dataset from the active filter variables panel. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$remove_filter_var(dataset_name = NULL, var_name = NULL)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{dataset_name}}{(character) The name of the dataset to remove the filter variable from. +\subsection{\code{TealAppDriver$remove_filter_var()}}{ + Remove an active filter variable of a dataset from the active filter variables panel. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$remove_filter_var(dataset_name = NULL, var_name = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{dataset_name}}{(character) The name of the dataset to remove the filter variable from. If \code{NULL}, all the filter variables will be removed.} - -\item{\code{var_name}}{(character) The name of the variable to remove from the filter panel. + \item{\code{var_name}}{(character) The name of the variable to remove from the filter panel. If \code{NULL}, all the filter variables of the dataset will be removed.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{TealAppDriver} object invisibly. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{TealAppDriver} object invisibly. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-set_active_filter_selection}{}}} -\subsection{Method \code{set_active_filter_selection()}}{ -Set the active filter values for a variable of a dataset in the active filter variable panel. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$set_active_filter_selection(dataset_name, var_name, input, ...)}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$set_active_filter_selection()}}{ + Set the active filter values for a variable of a dataset in the active filter variable panel. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$set_active_filter_selection(dataset_name, var_name, input, ...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{dataset_name}}{(character) The name of the dataset to set the filter value for.} + \item{\code{var_name}}{(character) The name of the variable to set the filter value for.} + \item{\code{input}}{The value to set the filter to.} + \item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{TealAppDriver} object invisibly. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{dataset_name}}{(character) The name of the dataset to set the filter value for.} - -\item{\code{var_name}}{(character) The name of the variable to set the filter value for.} - -\item{\code{input}}{The value to set the filter to.} - -\item{\code{...}}{Additional arguments to be passed to \code{shinytest2::AppDriver$set_inputs}} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{TealAppDriver} object invisibly. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_attr}{}}} -\subsection{Method \code{get_attr()}}{ -Extract \code{html} attribute (found by a \code{selector}). -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_attr(selector, attribute)}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$get_attr()}}{ + Extract \code{html} attribute (found by a \code{selector}). + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_attr(selector, attribute)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{selector}}{(\code{character(1)}) specifying the selector to be used to get the content of a specific node.} + \item{\code{attribute}}{(\code{character(1)}) name of an attribute to retrieve from a node specified by \code{selector}.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The \code{character} vector. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{selector}}{(\code{character(1)}) specifying the selector to be used to get the content of a specific node.} - -\item{\code{attribute}}{(\code{character(1)}) name of an attribute to retrieve from a node specified by \code{selector}.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -The \code{character} vector. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-get_html_rvest}{}}} -\subsection{Method \code{get_html_rvest()}}{ -Wrapper around \code{get_html} that passes the output directly to \code{rvest::read_html}. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$get_html_rvest(selector)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{selector}}{\code{(character(1))} passed to \code{get_html}.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -An XML document. +\subsection{\code{TealAppDriver$get_html_rvest()}}{ + Wrapper around \code{get_html} that passes the output directly to \code{rvest::read_html}. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$get_html_rvest(selector)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{selector}}{\code{(character(1))} passed to \code{get_html}.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + An XML document. Wrapper around \code{get_url()} method that opens the app in the browser. + } } -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-open_url}{}}} -\subsection{Method \code{open_url()}}{ -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$open_url()}\if{html}{\out{
}} +\subsection{\code{TealAppDriver$open_url()}}{ + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$open_url()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + Nothing. Opens the underlying teal app in the browser. + } } -\subsection{Returns}{ -Nothing. Opens the underlying teal app in the browser. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealAppDriver-wait_for_active_module_value}{}}} -\subsection{Method \code{wait_for_active_module_value()}}{ -Waits until a specified input, output, or export value. +\subsection{\code{TealAppDriver$wait_for_active_module_value()}}{ + Waits until a specified input, output, or export value. This function serves as a wrapper around the \code{wait_for_value} method, providing a more flexible interface for waiting on different types of values within the active module namespace. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealAppDriver$wait_for_active_module_value( + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealAppDriver$wait_for_active_module_value( input = rlang::missing_arg(), output = rlang::missing_arg(), export = rlang::missing_arg(), ... -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{input, output, export}}{A name of an input, output, or export value. +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{input, output, export}}{A name of an input, output, or export value. Only one of these parameters may be used.} - -\item{\code{...}}{Must be empty. Allows for parameter expansion. + \item{\code{...}}{Must be empty. Allows for parameter expansion. Parameter with additional value to passed in \code{wait_for_value}.} + } + \if{html}{\out{
}} + } } -\if{html}{\out{
}} -} -} + } diff --git a/man/TealReportCard.Rd b/man/TealReportCard.Rd index 8067c8002b..c7aee7a256 100644 --- a/man/TealReportCard.Rd +++ b/man/TealReportCard.Rd @@ -13,7 +13,7 @@ meta data. \examples{ ## ------------------------------------------------ -## Method `TealReportCard$append_src` +## Method `TealReportCard$append_src()` ## ------------------------------------------------ card <- TealReportCard$new()$append_src( @@ -22,7 +22,7 @@ card <- TealReportCard$new()$append_src( card$get_content()[[1]] ## ------------------------------------------------ -## Method `TealReportCard$append_encodings` +## Method `TealReportCard$append_encodings()` ## ------------------------------------------------ card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) @@ -34,137 +34,137 @@ card$get_content()[[1]] } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-TealReportCard-append_src}{\code{TealReportCard$append_src()}} -\item \href{#method-TealReportCard-append_fs}{\code{TealReportCard$append_fs()}} -\item \href{#method-TealReportCard-append_encodings}{\code{TealReportCard$append_encodings()}} -\item \href{#method-TealReportCard-clone}{\code{TealReportCard$clone()}} -} -} -\if{html}{\out{ -
Inherited methods + \itemize{ + \item \href{#method-TealReportCard-append_src}{\code{TealReportCard$append_src()}} + \item \href{#method-TealReportCard-append_fs}{\code{TealReportCard$append_fs()}} + \item \href{#method-TealReportCard-append_encodings}{\code{TealReportCard$append_encodings()}} + \item \href{#method-TealReportCard-clone}{\code{TealReportCard$clone()}} + } +} +\if{html}{\out{
Inherited methods -
-}} +
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealReportCard-append_src}{}}} -\subsection{Method \code{append_src()}}{ -Appends the source code to the \code{content} meta data of this \code{TealReportCard}. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealReportCard$append_src(src, ...)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{src}}{(\code{character(1)}) code as text.} - -\item{\code{...}}{any \code{rmarkdown} \code{R} chunk parameter and its value. +\subsection{\code{TealReportCard$append_src()}}{ + Appends the source code to the \code{content} meta data of this \code{TealReportCard}. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealReportCard$append_src(src, ...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{src}}{(\code{character(1)}) code as text.} + \item{\code{...}}{any \code{rmarkdown} \code{R} chunk parameter and its value. But \code{eval} parameter is always set to \code{FALSE}.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -Object of class \code{TealReportCard}, invisibly. -} -\subsection{Examples}{ -\if{html}{\out{
}} -\preformatted{card <- TealReportCard$new()$append_src( + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + Object of class \code{TealReportCard}, invisibly. + } + \subsection{Examples}{ + \if{html}{\out{
}} + \preformatted{card <- TealReportCard$new()$append_src( "plot(iris)" ) card$get_content()[[1]] } -\if{html}{\out{
}} - + \if{html}{\out{
}} + } } -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealReportCard-append_fs}{}}} -\subsection{Method \code{append_fs()}}{ -Appends the filter state list to the \code{content} and \code{metadata} of this \code{TealReportCard}. +\subsection{\code{TealReportCard$append_fs()}}{ + Appends the filter state list to the \code{content} and \code{metadata} of this \code{TealReportCard}. If the filter state list has an attribute named \code{formatted}, it appends it to the card otherwise it uses the default \code{yaml::as.yaml} to format the list. If the filter state list is empty, nothing is appended to the \code{content}. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealReportCard$append_fs(fs)}\if{html}{\out{
}} + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealReportCard$append_fs(fs)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{fs}}{(\code{teal_slices}) object returned from \code{\link[=teal_slices]{teal_slices()}} function.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + \code{self}, invisibly. + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{fs}}{(\code{teal_slices}) object returned from \code{\link[=teal_slices]{teal_slices()}} function.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -\code{self}, invisibly. -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealReportCard-append_encodings}{}}} -\subsection{Method \code{append_encodings()}}{ -Appends the encodings list to the \code{content} and \code{metadata} of this \code{TealReportCard}. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealReportCard$append_encodings(encodings)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{encodings}}{(\code{list}) list of encodings selections of the \code{teal} app.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -\code{self}, invisibly. -} -\subsection{Examples}{ -\if{html}{\out{
}} -\preformatted{card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) +\subsection{\code{TealReportCard$append_encodings()}}{ + Appends the encodings list to the \code{content} and \code{metadata} of this \code{TealReportCard}. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealReportCard$append_encodings(encodings)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{encodings}}{(\code{list}) list of encodings selections of the \code{teal} app.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + \code{self}, invisibly. + } + \subsection{Examples}{ + \if{html}{\out{
}} + \preformatted{card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) card$get_content()[[1]] - } -\if{html}{\out{
}} - + \if{html}{\out{
}} + } } -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TealReportCard-clone}{}}} -\subsection{Method \code{clone()}}{ -The objects of this class are cloneable with this method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TealReportCard$clone(deep = FALSE)}\if{html}{\out{
}} +\subsection{\code{TealReportCard$clone()}}{ + The objects of this class are cloneable with this method. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TealReportCard$clone(deep = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{deep}}{Whether to make a deep clone.} + } + \if{html}{\out{
}} + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{deep}}{Whether to make a deep clone.} -} -\if{html}{\out{
}} -} -} } diff --git a/man/dot-teal_favicon.Rd b/man/dot-teal_favicon.Rd index 9f20d66d8c..5c55019bfd 100644 --- a/man/dot-teal_favicon.Rd +++ b/man/dot-teal_favicon.Rd @@ -1,12 +1,8 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R -\docType{data} \name{.teal_favicon} \alias{.teal_favicon} \title{The default favicon for the teal app.} -\format{ -An object of class \code{character} of length 1. -} \usage{ .teal_favicon } diff --git a/man/module_snapshot_manager.Rd b/man/module_snapshot_manager.Rd index 723fcd3ccb..f3a34c9b0f 100644 --- a/man/module_snapshot_manager.Rd +++ b/man/module_snapshot_manager.Rd @@ -106,6 +106,15 @@ This is done on the app session, not the module session. Then that snapshot, and the previous snapshot history are dumped into the \code{values.rds} file in \verb{}. } +\section{Disabling the snapshot manager}{ + +The snapshot manager can be turned off application-wide by setting the +\code{teal.snapshot_manager.enable} option to \code{FALSE}. When disabled, both +\code{ui_snapshot_manager_panel()} and \code{srv_snapshot_manager_panel()} return \code{NULL}, +so the camera icon is not rendered and no snapshot logic is attached. +The option defaults to \code{TRUE} and is set when the \code{teal} package is loaded. +} + \author{ Aleksander Chlebowski } diff --git a/man/reporter_previewer_module.Rd b/man/reporter_previewer_module.Rd index 6f0ff86354..202a7838f7 100644 --- a/man/reporter_previewer_module.Rd +++ b/man/reporter_previewer_module.Rd @@ -24,8 +24,8 @@ It is now deprecated in favor of the options: \itemize{ \item \code{teal.reporter.nav_buttons = c("preview", "download", "load", "reset")} to control which buttons will be displayed in the drop-down. -\item \code{teal.reporter.rmd_output}: passed to \code{\link[teal.reporter:download_report_button]{teal.reporter::download_report_button_srv()}} -\item \code{teal.reporter.rmd_yaml_args}: passed to \code{\link[teal.reporter:download_report_button]{teal.reporter::download_report_button_srv()}} -\item \code{teal.reporter.global_knitr}: passed to \code{\link[teal.reporter:download_report_button]{teal.reporter::download_report_button_srv()}} +\item \code{teal.reporter.rmd_output}: passed to \code{\link[teal.reporter:download_report_button_srv]{teal.reporter::download_report_button_srv()}} +\item \code{teal.reporter.rmd_yaml_args}: passed to \code{\link[teal.reporter:download_report_button_srv]{teal.reporter::download_report_button_srv()}} +\item \code{teal.reporter.global_knitr}: passed to \code{\link[teal.reporter:download_report_button_srv]{teal.reporter::download_report_button_srv()}} } } diff --git a/man/teal-package.Rd b/man/teal-package.Rd index 49f09ba63c..27e798d529 100644 --- a/man/teal-package.Rd +++ b/man/teal-package.Rd @@ -27,6 +27,7 @@ Useful links: Authors: \itemize{ + \item Dawid Kaledkowski \email{dawid.kaledkowski@roche.com} (\href{https://orcid.org/0000-0001-9533-457X}{ORCID}) \item Pawel Rucki \email{pawel.rucki@roche.com} \item Aleksander Chlebowski \email{aleksander.chlebowski@contractors.roche.com} (\href{https://orcid.org/0000-0001-5018-6294}{ORCID}) \item Andre Verissimo \email{andre.verissimo@roche.com} (\href{https://orcid.org/0000-0002-2212-339X}{ORCID}) diff --git a/man/validate_input.Rd b/man/validate_input.Rd new file mode 100644 index 0000000000..5e2b6be2ed --- /dev/null +++ b/man/validate_input.Rd @@ -0,0 +1,186 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/validate_inputs.R +\name{validate_input} +\alias{validate_input} +\alias{need_input} +\alias{use_validate_input_js} +\title{Validate input} +\usage{ +validate_input( + inputId, + condition = function(x) TRUE, + message = "Check the input", + session = shiny::getDefaultReactiveDomain() +) + +need_input( + inputId, + condition = function(x) TRUE, + message = "Check the input", + session = shiny::getDefaultReactiveDomain() +) + +use_validate_input_js() +} +\arguments{ +\item{inputId}{(\code{character}) Character of input ID(s) to validate} + +\item{condition}{(\code{logical(1)}, \verb{function(x)}) Logical value or function returning logical value. +Condition should determine expected state, \code{FALSE} throws.} + +\item{message}{(\code{character(1)}) Character string of validation message to display.} + +\item{session}{Shiny session object} +} +\value{ +\code{NULL} or \code{shiny.silent.error} when condition is not met +} +\description{ +Validate a Shiny input and send validation messages to the client. +The message appears both in the input widget and in the output element +that calls \code{validate_input}. +} +\details{ +\itemize{ +\item \verb{validate_input():} + +Validate a Shiny input and send validation messages to the client. +The message appears both in the input widget and in the output element +that calls \code{validate_input}. +} + +\itemize{ +\item \code{need_input()}: Validate a Shiny input and returns a message to be used in a \code{\link[shiny:validate]{shiny::validate()}} call. +The message is sent to the client and appears both in the input widget. +To observe the message in the output element, \code{need_input()} should be called inside a +\code{shiny::validate()} call, e.g. via \code{validate(validate_input(...))}. +} + +\itemize{ +\item \code{use_validate_input_js()}: + +Include JavaScript for client-side input validation. +} +} +\examples{ +# Only run examples in interactive R sessions +options(device.ask.default = FALSE) + +ui <- fluidPage( + checkboxGroupInput("in1", "Check some letters", choices = head(LETTERS)), + selectizeInput("in2", "Select a state", choices = c("", state.name)), + use_validate_input_js(), + plotOutput("plot") +) + +server <- function(input, output) { + output$plot <- renderPlot({ + validate_input("in1", condition = function(x) length(x) > 0, "Check at least one letter!") + validate_input("in2", condition = function(x) x != "", "Please choose a state.") + plot(1:10, main = paste(c(input$in1, input$in2), collapse = ", ")) + }) +} + +if (interactive()) { + shinyApp(ui, server) +} + +my_module <- module( + label = "My Module", + datanames = NULL, + ui = function(id) { + ns <- NS(id) + tagList( + checkboxGroupInput(ns("letters"), "Select letters:", choices = head(LETTERS), inline = TRUE), + checkboxGroupInput(ns("letters2"), "Select letters:", choices = head(LETTERS), inline = TRUE), + tags$h3("Sample plot"), + plotOutput(ns("plot")) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + output$plot <- renderPlot({ + validate_input( + "letters", + condition = function(x) length(x) > 0, + message = "Select at least one letter." + ) + validate_input( + c("letters", "letters2"), + condition = function(x, y) all(!x \%in\% y), + message = "Letters in the first group should not be in the second group." + ) + tab <- rbind( + Group1 = as.integer(head(LETTERS) \%in\% input$letters), + Group2 = as.integer(head(LETTERS) \%in\% input$letters2) + ) + colnames(tab) <- head(LETTERS) + barplot( + tab, + beside = TRUE, legend.text = TRUE, main = "Selected letters per group", + col = c("steelblue", "tomato") + ) + }) + }) + } +) + +app <- init( + data = within(teal_data(), iris <- iris), + modules = my_module +) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +my_module <- module( + label = "My Module", + datanames = NULL, + ui = function(id) { + ns <- NS(id) + tagList( + checkboxGroupInput(ns("letters1"), "Select letters:", choices = head(LETTERS), inline = TRUE), + checkboxGroupInput(ns("letters2"), "Select letters:", choices = head(LETTERS), inline = TRUE), + tags$h3("Sample plot"), + plotOutput(ns("plot")) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + output$plot <- renderPlot({ + validate( + need_input( + "letters1", + condition = function(x) length(x) > 0, + message = "Select at least one letter." + ), + need_input( + c("letters1", "letters2"), + condition = function(x, y) all(!x \%in\% y), + message = "Letters in the first group should not be in the second group." + ) + ) + tab <- rbind( + Group1 = as.integer(head(LETTERS) \%in\% input$letters1), + Group2 = as.integer(head(LETTERS) \%in\% input$letters2) + ) + colnames(tab) <- head(LETTERS) + barplot( + tab, + beside = TRUE, legend.text = TRUE, main = "Selected letters per group", + col = c("steelblue", "tomato") + ) + }) + }) + } +) + +app <- init( + data = within(teal_data(), iris <- iris), + modules = my_module +) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 6bea6a044a..8c10ad949b 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -3305,6 +3305,23 @@ testthat::describe("srv_teal snapshot manager", { } ) }) + + testthat::it("is disabled by teal.snapshot_manager.enable = FALSE", { + withr::with_options(list(teal.snapshot_manager.enable = FALSE), { + testthat::expect_null(ui_snapshot_manager_panel("snapshot_manager_panel")) + shiny::testServer( + app = srv_teal, + args = list( + id = "test", + data = teal.data::teal_data(iris = iris), + modules = modules( + module("module_1", server = function(id, data) data) + ) + ), + expr = testthat::expect_null(snapshots) + ) + }) + }) }) testthat::describe("Datanames with special symbols", { diff --git a/tests/testthat/test-shinytest2-modules.R b/tests/testthat/test-shinytest2-modules.R index 497976c2d2..12f184e3d5 100644 --- a/tests/testthat/test-shinytest2-modules.R +++ b/tests/testthat/test-shinytest2-modules.R @@ -121,13 +121,14 @@ testthat::test_that("e2e: active tab is reflected in URL query string after navi example_module(label = "mod1"), example_module(label = "mod2") ) - ) + ), + teal_options = list(teal.enable_deep_linking = TRUE) ) on.exit(app$stop()) app$navigate_teal_tab("mod2") - testthat::expect_match(app$get_url(), "active_module=mod2") + testthat::expect_match(app$get_js("window.location.href"), "active_module=mod2") }) testthat::test_that("e2e: URL query string active_module switches to the specified tab", { @@ -139,7 +140,8 @@ testthat::test_that("e2e: URL query string active_module switches to the specifi example_module(label = "mod1"), example_module(label = "mod2") ) - ) + ), + teal_options = list(teal.enable_deep_linking = TRUE) ) on.exit(app$stop()) diff --git a/tests/testthat/test-shinytest2-teal_data_module.R b/tests/testthat/test-shinytest2-teal_data_module.R index c065d436a7..5676ad022d 100644 --- a/tests/testthat/test-shinytest2-teal_data_module.R +++ b/tests/testthat/test-shinytest2-teal_data_module.R @@ -98,7 +98,7 @@ testthat::test_that("e2e: teal_data_module doesn't auto-close when `once=FALSE` app$stop() }) -testthat::test_that("e2e: teal_data_module doesn't auto-close when `once=FALSE` and data is ready (no submit)", { +testthat::test_that("e2e: teal_data_module auto-closes modal when `once=FALSE` and data is ready (no submit)", { skip_if_too_deep(5) app <- TealAppDriver$new( init( @@ -106,11 +106,11 @@ testthat::test_that("e2e: teal_data_module doesn't auto-close when `once=FALSE` modules = example_module(label = "Example Module") ) ) - app$expect_visible(".teal-data-module-popup") + testthat::expect_null(app$get_html(".teal-data-module-popup")) app$stop() }) -testthat::test_that("e2e: teal_data_module modal close button is enabled from disabled when data is ready", { +testthat::test_that("e2e: teal_data_module modal stays visible on startup when `once=FALSE` and `need_submit = TRUE`", { skip_if_too_deep(5) app <- TealAppDriver$new( init( @@ -118,13 +118,8 @@ testthat::test_that("e2e: teal_data_module modal close button is enabled from di modules = example_module(label = "Example Module") ) ) - - testthat::expect_identical( - app$get_attr("#teal-close_teal_data_module_modal", "disabled"), - "disabled" - ) - app$click("teal-teal_data_module-submit") - testthat::expect_true(is.na(app$get_attr("#teal-close_teal_data_module_modal", "disabled"))) + app$expect_visible(".teal-data-module-popup") + app$expect_visible("#teal-teal_data_module-submit") app$stop() }) diff --git a/tests/testthat/test-shinytest2-validate_input.R b/tests/testthat/test-shinytest2-validate_input.R new file mode 100644 index 0000000000..2436964b20 --- /dev/null +++ b/tests/testthat/test-shinytest2-validate_input.R @@ -0,0 +1,334 @@ +testthat::skip_if_not_installed("shinytest2") +testthat::skip_if_not_installed("rvest") + +testthat::test_that("e2e: validate_input displays validation message when input is invalid, no message when valid", { + skip_if_too_deep(5) + + validation_module <- function(label = "validation test") { + module( + label = label, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + output$plot <- renderPlot({ + validate_input( + inputId = "number", + condition = function(x) !is.null(x) && as.numeric(x) > 5, + message = "Please select a number greater than 5", + session = session + ) + plot(1:10) + }) + }) + }, + ui = function(id) { + ns <- NS(id) + tagList( + numericInput(ns("number"), "Enter a number:", value = 3, min = 1, max = 10), + plotOutput(ns("plot")) + ) + } + ) + } + + app_driver <- TealAppDriver$new( + init( + data = simple_teal_data(), + modules = validation_module(label = "Validation Test") + ) + ) + withr::defer(app_driver$stop()) + + # Check that validation error is displayed in the output + error_text <- app_driver$get_text(".shiny-output-error-validation") + testthat::expect_match(error_text, "Please select a number greater than 5") + + + # Update input to valid value + app_driver$set_input(app_driver$namespaces()$module("number"), 7) + app_driver$wait_for_idle() + + # Check that validation error is gone + testthat::expect_null(app_driver$get_text(".shiny-output-error-validation")) +}) + +testthat::test_that("e2e: validate_input validates many inputs (and linked output) when they return invalid value", { + skip_if_too_deep(5) + + multi_validation_module <- function(label = "multi validation") { + module( + label = label, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + output$result <- renderText({ + validate_input( + inputId = c("input1", "input2"), + condition = function(x, y) identical(x, y), + message = "x and y must be identical", + session = session + ) + paste("Valid inputs:", input$input1, input$input2) + }) + }) + }, + ui = function(id) { + ns <- NS(id) + tagList( + numericInput(ns("input1"), "Enter x:", value = 4, min = 0, max = 100), + numericInput(ns("input2"), "Enter y:", value = 6, min = 0, max = 100), + textOutput(ns("result")) + ) + } + ) + } + + app_driver <- TealAppDriver$new( + init( + data = simple_teal_data(), + modules = multi_validation_module(label = "Multi Validation") + ) + ) + withr::defer(app_driver$stop()) + + error_text <- app_driver$get_text(".shiny-output-error-validation") + testthat::expect_match(error_text, "x and y must be identical") + testthat::expect_match( + app_driver$get_text(app_driver$namespaces(TRUE)$module("result")), + "x and y must be identical" + ) + + # Update input2 to valid value + app_driver$set_input(app_driver$namespaces()$module("input1"), 5) + app_driver$set_input(app_driver$namespaces()$module("input2"), 5) + app_driver$wait_for_idle() + + # Check that validation passes and result is shown + error_text <- app_driver$get_text(".shiny-output-error-validation") + testthat::expect_null(error_text) + + testthat::expect_match( + app_driver$get_text(app_driver$namespaces(TRUE)$module("result")), + "Valid inputs: 5 5" + ) +}) + +testthat::describe("e2e: validate_input validates generic fields", { + skip_if_too_deep(5) + all_inputs_mod <- module( + label = "all inputs", + ui = function(id) { + ns <- NS(id) + tagList( + selectInput(ns("select"), "Select Input:", choices = c("A", "B", "C")), + selectizeInput(ns("selectize"), "Selectize Input:", choices = c("X", "Y", "Z")), + radioButtons(ns("radio"), "Radio Buttons:", choices = c("Option 1", "Option 2")), + checkboxGroupInput(ns("checkbox_group"), "Checkbox Group:", choices = c("Check 1", "Check 2")), + checkboxInput(ns("checkbox"), "Checkbox Input"), + dateInput(ns("date"), "Date Input:"), + dateRangeInput(ns("date_range"), "Date Range Input:") + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + validate( + need_input("select", function(x) identical(x, "B"), "select must be B"), + need_input("selectize", function(x) identical(x, "Y"), "selectize must be Y"), + need_input("radio", function(x) identical(x, "Option 2"), "radio must be Option 2"), + need_input( + "checkbox_group", + function(x) identical(x, c("Check 2")), + "checkbox_group must be Check 2" + ), + need_input("checkbox", function(x) identical(x, TRUE), "checkbox must be TRUE"), + need_input("date", function(x) identical(x, as.Date("2024-01-01")), "date must be 2024-01-01"), + need_input( + "date_range", + function(x) identical(x, c(as.Date("2024-01-01"), as.Date("2024-01-31"))), + "date_range must be 2024-01-01 to 2024-01-31" + ) + ) + data() + }) + }) + } + ) + + app_driver <- TealAppDriver$new(init(data = simple_teal_data(), modules = all_inputs_mod)) + withr::defer(app_driver$stop()) + + it("selectInput", { + message <- "select must be B" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("select"), "B") + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) + + it("selectizeInput", { + message <- "selectize must be Y" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("selectize"), "Y") + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) + + it("radioButtons", { + message <- "radio must be Option 2" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("radio"), "Option 2") + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) + + it("checkboxGroupInput", { + message <- "checkbox_group must be Check 2" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("checkbox_group"), "Check 2") + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) + + it("checkboxInput", { + message <- "checkbox must be TRUE" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("checkbox"), TRUE) + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) + + it("dateInput", { + message <- "date must be 2024-01-01" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("date"), "2024-01-01") + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) + + it("dateRangeInput", { + message <- "date_range must be 2024-01-01 to 2024-01-31" + testthat::expect_match(app_driver$get_text(".shiny-output-error"), message, fixed = TRUE, all = FALSE) + app_driver$set_input(app_driver$namespaces()$module("date_range"), c("2024-01-01", "2024-01-31")) + testthat::expect_no_match(app_driver$get_text(".shiny-output-error") %||% character(0L), message, fixed = TRUE) + }) +}) + +testthat::test_that("validate_input shinytest2 - sequence of errors with parallel validation", { + my_module <- module( + label = "My Module", + datanames = NULL, + ui = function(id) { + ns <- NS(id) + tagList( + checkboxGroupInput(ns("letters1"), "Select letters:", choices = head(LETTERS), inline = TRUE), + checkboxGroupInput(ns("letters2"), "Select letters:", choices = head(LETTERS), inline = TRUE), + tags$h3("Sample plot"), + plotOutput(ns("plot")) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + output$plot <- renderPlot({ + validate( + need_input( + "letters1", + condition = length(input$letters1) > 0, + message = "Select at least one letter." + ), + need_input( + c("letters1", "letters2"), + condition = function(x, y) all(!x %in% y), + message = "Letters in the first group should not be in the second group." + ) + ) + tab <- rbind( + Group1 = as.integer(head(LETTERS) %in% input$letters1), + Group2 = as.integer(head(LETTERS) %in% input$letters2) + ) + colnames(tab) <- head(LETTERS) + barplot( + tab, + beside = TRUE, legend.text = TRUE, main = "Selected letters per group", + col = c("steelblue", "tomato") + ) + }) + }) + } + ) + + app_driver <- TealAppDriver$new( + app = init( + data = within(teal_data(), iris <- iris), + modules = my_module + ) + ) + withr::defer(app_driver$stop()) + + # Shows 1 error messages in initialization + testthat::expect_equal( + app_driver$get_text( + "#teal-teal_modules-nav-my_module-module-letters1 ~ .shiny-output-error.shiny-input-validation-error" + ), + "Select at least one letter." + ) + testthat::expect_length(app_driver$get_text(".shiny-output-error.shiny-input-validation-error"), 1) + # Shows error in output + testthat::expect_equal( + app_driver$get_text("#teal-teal_modules-nav-my_module-module-plot"), + "Select at least one letter." + ) + # after clicking on first letter no errors appear + app_driver$set_inputs("teal-teal_modules-nav-my_module-module-letters1" = "A") + testthat::expect_null(app_driver$get_text(".shiny-output-error.shiny-input-validation-error")) + + # after clicking on the same letter in the second group validation error appears + app_driver$set_inputs("teal-teal_modules-nav-my_module-module-letters2" = "A") + testthat::expect_equal( + app_driver$get_text(".shiny-output-error.shiny-input-validation-error"), + rep("Letters in the first group should not be in the second group.", 2) + ) +}) + +testthat::test_that("validate_input shinytest2 - stale messages are not shown", { + my_module <- module( + label = "My Module", + datanames = NULL, + ui = function(id) { + ns <- NS(id) + tagList( + textInput(ns("letters1"), "Select letters:", value = "A"), + textInput(ns("letters2"), "Select letters:", value = ""), + uiOutput(ns("validation_message")) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + output$validation_message <- renderUI({ + validate( + need_input( + "letters1", + condition = nchar(input$letters1) > 0, + message = "Select at least one letter for first input." + ) + ) + validate( + need_input( + c("letters2"), + condition = nchar(input$letters2) > 0, + message = "Select at least one letter for second input." + ) + ) + tags$div("All inputs are valid.") + }) + }) + } + ) + + app_driver <- TealAppDriver$new( + app = init( + data = within(teal_data(), iris <- iris), + modules = my_module + ) + ) + withr::defer(app_driver$stop()) + + app_driver$set_active_module_input("letters1", character(0)) + app_driver$set_active_module_input("letters2", "B") + testthat::expect_no_match( + app_driver$get_text(".shiny-input-validation-error"), + "Select at least one letter for second input." + ) +}) diff --git a/tests/testthat/test-validate_input.R b/tests/testthat/test-validate_input.R new file mode 100644 index 0000000000..fbc3738611 --- /dev/null +++ b/tests/testthat/test-validate_input.R @@ -0,0 +1,254 @@ +testthat::test_that("validate_input(inputId) has to be a character(1)", { + testthat::expect_error( + validate_input(inputId = character(0)), + "Assertion on 'inputId' failed" + ) + testthat::expect_error( + validate_input(inputId = 123), + "Assertion on 'inputId' failed" + ) + testthat::expect_error( + validate_input(inputId = NULL), + "Assertion on 'inputId' failed" + ) +}) + +testthat::test_that("validate_input(condition) has to be a logical(1) or function with nargs = length(inputId)", { + testthat::expect_error( + validate_input(inputId = "test", condition = "not_logical"), + "Assertion failed" + ) + testthat::expect_error( + validate_input(inputId = "test", condition = c(TRUE, FALSE)), + "Assertion failed" + ) + + testthat::expect_error( + validate_input(inputId = "test", condition = function(x, y) TRUE), + "Assertion failed" + ) + + testthat::expect_error( + validate_input(inputId = c("test", "test2"), condition = function(x) TRUE), + "Assertion failed" + ) +}) + +testthat::test_that("validate_input(message) has to be a character(1)", { + testthat::expect_error( + validate_input(inputId = "test", message = 123), + "Assertion on 'message' failed" + ) + testthat::expect_error( + validate_input(inputId = "test", message = c("a", "b")), + "Assertion on 'message' failed" + ) +}) + +testthat::test_that("validate_input returns NULL when condition is TRUE", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + result <- reactive({ + validate_input( + inputId = "test_input", + condition = TRUE, + message = "Test message", + session = session + ) + }) + }) + } + + shiny::testServer( + app = test_module, + args = list(id = "test"), + expr = testthat::expect_null(result()) + ) +}) + +testthat::test_that("validate_input throws `message` as shiny.silent.error when `condition = FALSE`", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + result <- reactive({ + validate_input( + inputId = "test_input", + condition = FALSE, + message = "Test message", + session = session + ) + }) + }) + } + + shiny::testServer( + app = test_module, + args = list(id = "test"), + expr = testthat::expect_error(result(), "Test message") + ) +}) + +testthat::test_that("validate_input works with function condition that returns `TRUE` for input with `inputId`", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + result <- reactive({ + validate_input( + inputId = "test_input", + condition = function(x) identical(x, "valid_value"), + message = "Input is invalid", + session = session + ) + }) + }) + } + + shiny::testServer( + app = test_module, + expr = { + # Set up input value + session$setInputs(test_input = "valid_value") + testthat::expect_null(result()) + }, + args = list(id = "test") + ) +}) + +testthat::test_that("validate_input works with function condition that returns `FALSE` for input with `inputId`", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + result <- reactive({ + validate_input( + inputId = "test_input", + condition = function(x) identical(x, "valid_value"), + message = "Input is invalid", + session = session + ) + }) + }) + } + + shiny::testServer( + app = test_module, + expr = { + # Set up input value + session$setInputs(test_input = "invalid_value") + testthat::expect_error(result(), "Input is invalid") + }, + args = list(id = "test") + ) +}) + +testthat::test_that("validate_input with multiple inputIds throws shiny.silent.error when any input is invalid", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + result <- reactive({ + validate_input( + inputId = c("test_input1", "test_input2"), + condition = function(x, y) identical(x, y), + message = "Are not identical", + session = session + ) + }) + }) + } + + shiny::testServer( + app = test_module, + expr = { + # Set up input value + session$setInputs(test_input1 = 99, test_input2 = 98) + testthat::expect_error(result(), "Are not identical") + }, + args = list(id = "test") + ) +}) + +testthat::test_that("validate_input with multiple inputIds doesn't throw shiny.silent.error if all inputs are valid", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + result <- reactive({ + validate_input( + inputId = c("test_input1", "test_input2"), + condition = function(x, y) identical(x, y), + message = "Input is invalid", + session = session + ) + }) + }) + } + + shiny::testServer( + app = test_module, + expr = { + # Set up input value + session$setInputs(test_input1 = 99, test_input2 = 99) + testthat::expect_no_error(result()) + }, + args = list(id = "test") + ) +}) + +testthat::describe("validate with multiple need_input", { + it("1 validation", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + reactive({ + validate( + need_input( + inputId = "test_input", + condition = function(x) is.null(x), + message = "input must be null", + session = session + ) + ) + }) + }) + } + + shiny::testServer( + app = test_module, + args = list(id = "test"), + expr = { + # Set up input value + testthat::expect_no_error(session$returned()) + session$setInputs(test_input = "invalid_value") + testthat::expect_error(session$returned(), "input must be null") + } + ) + }) + + it("2 validations", { + test_module <- function(id) { + moduleServer(id, function(input, output, session) { + reactive({ + validate( + need_input( + inputId = "test_input1", + condition = function(x) is.null(x), + message = "input1 must be null", + session = session + ), + need_input( + inputId = "test_input2", + condition = function(x) is.null(x), + message = "input2 must be null", + session = session + ) + ) + }) + }) + } + + shiny::testServer( + app = test_module, + args = list(id = "test"), + expr = { + # Set up input value + testthat::expect_no_error(session$returned()) + session$setInputs(test_input1 = "invalid_value", test_input2 = "invalid_value") + testthat::expect_error(session$returned(), "input1 must be null\ninput2 must be null") + session$setInputs(test_input1 = NULL, test_input2 = "invalid_value") + testthat::expect_error(session$returned(), "^input2 must be null$") + } + ) + }) +}) diff --git a/vignettes/teal-options.Rmd b/vignettes/teal-options.Rmd index fe6bda7961..4387a1397c 100644 --- a/vignettes/teal-options.Rmd +++ b/vignettes/teal-options.Rmd @@ -172,6 +172,12 @@ This option enables URL-based module navigation in `teal` apps. When set to `TRU Default: `FALSE`. +### `teal.snapshot_manager.enable` (`logical`) + +This option controls whether the Snapshot Manager panel (the camera icon in the tabset bar) is rendered in a `teal` app. When set to `FALSE`, both `ui_snapshot_manager_panel()` and `srv_snapshot_manager_panel()` return `NULL`, so the panel is not displayed and no snapshot logic is attached. + +Default: `TRUE`. + # Deprecated options ### `teal_logging`