mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-13 08:57:57 -05:00
Compare commits
7 Commits
tab-docs
...
moduleServ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a036aa4607 | ||
|
|
2c2ca4b58e | ||
|
|
c2c0a0d836 | ||
|
|
ed93d42a6e | ||
|
|
6fa332aa77 | ||
|
|
14b572e115 | ||
|
|
946435f25d |
@@ -69,6 +69,19 @@ extract <- function(promise) {
|
|||||||
stop("Single-bracket indexing of mockclientdata is not allowed.")
|
stop("Single-bracket indexing of mockclientdata is not allowed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#' @noRd
|
||||||
|
patchModuleFunction <- function(module) {
|
||||||
|
body(module) <- rlang::expr({
|
||||||
|
withr::with_options(base::list(`shiny.allowoutputreads` = TRUE), {
|
||||||
|
session$setEnv(base::environment())
|
||||||
|
session$setReturned({
|
||||||
|
!!!body(module)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
module
|
||||||
|
}
|
||||||
|
|
||||||
#' Mock Shiny Session
|
#' Mock Shiny Session
|
||||||
#'
|
#'
|
||||||
#' @description
|
#' @description
|
||||||
@@ -80,7 +93,6 @@ extract <- function(promise) {
|
|||||||
MockShinySession <- R6Class(
|
MockShinySession <- R6Class(
|
||||||
'MockShinySession',
|
'MockShinySession',
|
||||||
portable = FALSE,
|
portable = FALSE,
|
||||||
class = FALSE,
|
|
||||||
public = list(
|
public = list(
|
||||||
#' @field env The environment associated with the session.
|
#' @field env The environment associated with the session.
|
||||||
env = NULL,
|
env = NULL,
|
||||||
@@ -101,7 +113,8 @@ MockShinySession <- R6Class(
|
|||||||
userData = NULL,
|
userData = NULL,
|
||||||
#' @field progressStack A stack of progress objects
|
#' @field progressStack A stack of progress objects
|
||||||
progressStack = 'Stack',
|
progressStack = 'Stack',
|
||||||
|
#' @field TRUE when a moduleServer()-based module is under test
|
||||||
|
isModuleServer = FALSE,
|
||||||
#' @description Create a new MockShinySession
|
#' @description Create a new MockShinySession
|
||||||
initialize = function() {
|
initialize = function() {
|
||||||
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
|
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
|
||||||
@@ -381,16 +394,38 @@ MockShinySession <- R6Class(
|
|||||||
flushReact = function(){
|
flushReact = function(){
|
||||||
private$flush()
|
private$flush()
|
||||||
},
|
},
|
||||||
|
setEnv = function(env) {
|
||||||
|
self$env <- env
|
||||||
|
},
|
||||||
|
setReturned = function(value) {
|
||||||
|
private$returnedVal <- value
|
||||||
|
private$flush()
|
||||||
|
value
|
||||||
|
},
|
||||||
#' @description Create and return a namespace-specific session proxy.
|
#' @description Create and return a namespace-specific session proxy.
|
||||||
#' @param namespace Character vector indicating a namespace.
|
#' @param namespace Character vector indicating a namespace.
|
||||||
makeScope = function(namespace) {
|
makeScope = function(namespace) {
|
||||||
ns <- NS(namespace)
|
ns <- NS(namespace)
|
||||||
createSessionProxy(
|
proxy <- createSessionProxy(
|
||||||
self,
|
self,
|
||||||
input = .createReactiveValues(private$.input, readonly = TRUE, ns = ns),
|
input = .createReactiveValues(private$.input, readonly = TRUE, ns = ns),
|
||||||
output = structure(.createOutputWriter(self, ns = ns), class = "shinyoutput"),
|
output = structure(.createOutputWriter(self, ns = ns), class = "shinyoutput"),
|
||||||
makeScope = function(namespace) self$makeScope(ns(namespace))
|
makeScope = function(namespace) self$makeScope(ns(namespace)),
|
||||||
|
env = NULL,
|
||||||
|
returned = NULL,
|
||||||
|
setEnv = function(env) assign("env", env, envir = proxy),
|
||||||
|
setReturned = function(value) {
|
||||||
|
assign("returned", value, envir = proxy)
|
||||||
|
private$flush()
|
||||||
|
value
|
||||||
|
},
|
||||||
|
setInputs = function(...) {
|
||||||
|
args <- list(...)
|
||||||
|
names(args) <- ns(names(args))
|
||||||
|
do.call(self$setInputs, args)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
proxy
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
private = list(
|
private = list(
|
||||||
|
|||||||
24
R/modules.R
24
R/modules.R
@@ -121,15 +121,33 @@ createSessionProxy <- function(parentSession, ...) {
|
|||||||
#'
|
#'
|
||||||
#' @export
|
#' @export
|
||||||
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) {
|
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) {
|
||||||
callModule(module, id, session = session)
|
if (inherits(sessionFor(session), "MockShinySession")) {
|
||||||
|
module <- patchModuleFunction(module)
|
||||||
|
isolate(callModule(module, id, session = session))
|
||||||
|
} else {
|
||||||
|
callModule(module, id, session = session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#' @noRd
|
||||||
|
sessionFor <- function(session) {
|
||||||
|
if (inherits(session, c("MockShinySession", "ShinySession")))
|
||||||
|
return(session)
|
||||||
|
|
||||||
|
if (!inherits(session, "session_proxy"))
|
||||||
|
stop("session must be a ShinySession, MockShinySession, or session_proxy object.")
|
||||||
|
|
||||||
|
while (inherits(session, "session_proxy"))
|
||||||
|
session <- session$parent
|
||||||
|
|
||||||
|
session
|
||||||
|
}
|
||||||
|
|
||||||
#' @rdname moduleServer
|
#' @rdname moduleServer
|
||||||
#' @export
|
#' @export
|
||||||
callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
|
callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
|
||||||
if (!inherits(session, "ShinySession") && !inherits(session, "session_proxy")) {
|
if (!inherits(session, c("ShinySession", "MockShinySession", "session_proxy"))) {
|
||||||
stop("session must be a ShinySession or session_proxy object.")
|
stop("session must be a ShinySession, MockShinySession, or session_proxy object.")
|
||||||
}
|
}
|
||||||
childScope <- session$makeScope(id)
|
childScope <- session$makeScope(id)
|
||||||
|
|
||||||
|
|||||||
@@ -64,38 +64,27 @@ testModule <- function(module, expr, ...) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#' @noRd
|
isOldModule <- function(func) {
|
||||||
#' @importFrom withr with_options
|
stopifnot(is.function(func))
|
||||||
.testModule <- function(module, quosure, dots, env) {
|
required <- c("input", "output", "session")
|
||||||
# Modify the module function locally by inserting `session$env <-
|
declared <- names(formals(func))
|
||||||
# environment()` at the beginning of its body. The dynamic environment of the
|
setequal(required, intersect(required, declared))
|
||||||
# module function is saved so that it may be referenced after the module
|
}
|
||||||
# function has returned. The saved dynamic environment is the basis for the
|
|
||||||
# `data` argument of tidy_eval() when used below to evaluate `quosure`, the
|
|
||||||
# test code expression.
|
|
||||||
body(module) <- rlang::expr({
|
|
||||||
session$env <- base::environment()
|
|
||||||
!!!body(module)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
#' @noRd
|
||||||
|
.testModule <- function(module, quosure, dots, env) {
|
||||||
session <- MockShinySession$new()
|
session <- MockShinySession$new()
|
||||||
on.exit(if (!session$isClosed()) session$close())
|
on.exit(if (!session$isClosed()) session$close())
|
||||||
args <- append(dots, list(input = session$input, output = session$output, session = session))
|
|
||||||
|
|
||||||
isolate(
|
if (isOldModule(module)) {
|
||||||
withReactiveDomain(
|
module <- patchModuleFunction(module)
|
||||||
session,
|
args <- append(dots, list(input = session$input, output = session$output, session = session))
|
||||||
withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
|
} else {
|
||||||
# Assigning to `$returned` causes a flush to happen automatically.
|
args <- dots
|
||||||
session$returned <- do.call(module, args)
|
}
|
||||||
})
|
|
||||||
)
|
isolate(withReactiveDomain(session, do.call(module, args)))
|
||||||
)
|
|
||||||
|
|
||||||
# Evaluate `quosure` in a reactive context, and in the provided `env`, but
|
|
||||||
# with `env` masked by a shallow view of `session$env`, the environment that
|
|
||||||
# was saved when the module function was invoked. flush is not needed before
|
|
||||||
# entering the loop because the first expr executed is `{`.
|
|
||||||
isolate({
|
isolate({
|
||||||
withReactiveDomain(
|
withReactiveDomain(
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ reference:
|
|||||||
desc: Functions for modularizing Shiny apps
|
desc: Functions for modularizing Shiny apps
|
||||||
contents:
|
contents:
|
||||||
- NS
|
- NS
|
||||||
- callModule
|
- moduleServer
|
||||||
- title: Embedding
|
- title: Embedding
|
||||||
desc: Functions that are intended for third-party packages that embed Shiny applications.
|
desc: Functions that are intended for third-party packages that embed Shiny applications.
|
||||||
contents:
|
contents:
|
||||||
|
|||||||
23
tests/testthat/test-test-moduleServer.R
Normal file
23
tests/testthat/test-test-moduleServer.R
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
context("testModule-moduleServer")
|
||||||
|
|
||||||
|
test_that("New-style modules work", {
|
||||||
|
counterServer <- local({
|
||||||
|
function(id) {
|
||||||
|
moduleServer(id, function(input, output, session) {
|
||||||
|
count <- reactiveVal(0)
|
||||||
|
observeEvent(input$button, {
|
||||||
|
count(count() + 1)
|
||||||
|
})
|
||||||
|
output$out <- renderText({
|
||||||
|
count()
|
||||||
|
})
|
||||||
|
count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
testModule(counterServer, {
|
||||||
|
input$setInputs(button = 0)
|
||||||
|
input$setInputs(button = 1)
|
||||||
|
expect_equal(count(), 1)
|
||||||
|
}, id = "foob")
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user