Compare commits

...

26 Commits

Author SHA1 Message Date
Alan Dipert
a036aa4607 still broken, progress on new modules and proxied mocksession 2020-03-07 00:10:07 +00:00
Alan Dipert
2c2ca4b58e simplifications 2020-03-06 23:40:27 +00:00
Alan Dipert
c2c0a0d836 A little churn 2020-03-06 00:35:35 +00:00
Alan Dipert
ed93d42a6e Simplify differentiation strategy 2020-03-05 21:24:38 +00:00
Alan Dipert
6fa332aa77 Add changes and a failing test 2020-03-05 19:46:35 +00:00
Alan Dipert
14b572e115 Passing existing tests 2020-03-05 19:25:33 +00:00
Alan Dipert
946435f25d Add class to ShinyMockSession and fix tests 2020-03-05 18:11:42 +00:00
Winston Chang
01491cc696 Merge pull request #2772 from rstudio/test-nest-testModule-testServer
Small changes to testModule()/testServer() semantics
2020-03-04 14:58:30 -06:00
Alan Dipert
568a3f28cf Change test to not be locale-dependent 2020-03-04 20:48:21 +00:00
Winston Chang
02219df480 Merge pull request #2773 from rstudio/wch-module
Add moduleServer function
2020-03-04 09:24:58 -06:00
Alan Dipert
0975a61725 Add test to mitigate shadow with unquote 2020-03-03 22:32:59 +00:00
Alan Dipert
2fbb2ac77b Merge remote-tracking branch 'origin/master' into test-nest-testModule-testServer 2020-03-03 19:25:49 +00:00
Alan Dipert
2832db7aba New session$env test 2020-03-03 19:25:14 +00:00
Alan Dipert
18f2471d7c Fix some roxygen errors 2020-03-03 19:08:49 +00:00
Alan Dipert
ea28f5a61b Minor changes and tests 2020-03-03 18:58:42 +00:00
Winston Chang
fe9cc6038e Merge pull request #2774 from rstudio/no-slack-notifications
Remove slack notifications
2020-03-03 12:09:39 -06:00
Barret Schloerke
5ed335c499 Remove slack notifications 2020-03-03 12:42:48 -05:00
Alan Dipert
fd04b97496 Fix global reference test inside testServer 2020-03-02 17:56:47 +00:00
Alan Dipert
4c9d281b59 Subtle change to .testModule() semantics 2020-02-28 22:49:37 +00:00
Joe Cheng
2ee06a7cbf Revert "Support shiny.autoreload even when there are errors"
This reverts commit cf2ba90b1d.
2020-02-22 12:21:58 -08:00
Joe Cheng
cf2ba90b1d Support shiny.autoreload even when there are errors 2020-02-22 12:20:12 -08:00
Barret Schloerke
c69f34d1e2 update js files (version bump) 2020-02-18 13:47:02 -05:00
Barret Schloerke
ccfcc5d8b4 add news item 2020-02-18 13:47:02 -05:00
Barret Schloerke
210c248264 bump version 2020-02-18 13:47:02 -05:00
Barret Schloerke
e3258657d0 Invoke onSessionEnded callbacks with self reactive domain 2020-02-18 13:47:02 -05:00
Barret Schloerke
dbc518bf53 Fix broken timer tests and check htmltools docs (#2758)
* Adjust time so that it's in seconds and use expect_true to use regular R dispatch

* Execute './tools/updateHtmltoolsMan.R'

* add check for htmltools docs being up to date
2020-02-14 11:36:51 -05:00
18 changed files with 227 additions and 56 deletions

View File

@@ -5,6 +5,7 @@ matrix:
r: release r: release
r_packages: r_packages:
- devtools - devtools
- rprojroot
script: ./tools/checkDocsCurrent.sh script: ./tools/checkDocsCurrent.sh
- name: "Javascript check" - name: "Javascript check"
language: node_js language: node_js
@@ -25,7 +26,3 @@ notifications:
email: email:
on_success: change on_success: change
on_failure: change on_failure: change
slack:
on_success: change
secure: QoM0+hliVC4l2HYv126AkljG/uFvgwayW9IpuB5QNqjSukM122MhMDL7ZuMB9a2vWP24juzOTXiNIymgEspfnvvAMnZwYRBNWkuot2m8HIR2B9UjQLiztFnN1EAT+P+thz8Qax9TV2SOfXb2S2ZOeZmRTVkJctxkL8heAZadIC4=
on_pull_requests: false

View File

@@ -1,7 +1,7 @@
Package: shiny Package: shiny
Type: Package Type: Package
Title: Web Application Framework for R Title: Web Application Framework for R
Version: 1.4.0.9001 Version: 1.4.0.9002
Authors@R: c( Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"), person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"), person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),

View File

@@ -17,6 +17,8 @@ shiny 1.4.0.9001
* Added a label to observer that auto-reloads `R/` directory to avoid confusion when using `reactlog`. ([#58](https://github.com/rstudio/reactlog/issues/58)) * Added a label to observer that auto-reloads `R/` directory to avoid confusion when using `reactlog`. ([#58](https://github.com/rstudio/reactlog/issues/58))
* `getDefaultReactiveDomain()` can now be called inside a `session$onSessionEnded` callback and will return the calling `session` information. ([#2757](https://github.com/rstudio/shiny/pull/2757))
### Bug fixes ### Bug fixes
* Fixed [#2606](https://github.com/rstudio/shiny/issues/2606): `debounce()` would not work properly if the code in the reactive expression threw an error on the first run. ([#2652](https://github.com/rstudio/shiny/pull/2652)) * Fixed [#2606](https://github.com/rstudio/shiny/issues/2606): `debounce()` would not work properly if the code in the reactive expression threw an error on the first run. ([#2652](https://github.com/rstudio/shiny/pull/2652))

View File

@@ -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,14 +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.
#' @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(

View File

@@ -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)

View File

@@ -962,7 +962,9 @@ ShinySession <- R6Class(
output$suspend() output$suspend()
} }
# ..stacktraceon matches with the top-level ..stacktraceoff.. # ..stacktraceon matches with the top-level ..stacktraceoff..
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE) withReactiveDomain(self, {
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
})
}, },
isClosed = function() { isClosed = function() {
return(self$closed) return(self$closed)

View File

@@ -14,6 +14,7 @@
#' @param ... Additional arguments to pass to the module function. These #' @param ... Additional arguments to pass to the module function. These
#' arguments are processed with [rlang::list2()] and so are #' arguments are processed with [rlang::list2()] and so are
#' _[dynamic][rlang::dyn-dots]_. #' _[dynamic][rlang::dyn-dots]_.
#' @return The result of evaluating `expr`.
#' @include mock-session.R #' @include mock-session.R
#' @rdname testModule #' @rdname testModule
#' @examples #' @examples
@@ -55,53 +56,47 @@
#' }, !!multiplier_arg_name := 2, !!!more_args) #' }, !!multiplier_arg_name := 2, !!!more_args)
#' @export #' @export
testModule <- function(module, expr, ...) { testModule <- function(module, expr, ...) {
expr <- substitute(expr) .testModule(
.testModule(module, expr, ...) module,
quosure = rlang::enquo(expr),
dots = rlang::list2(...),
env = rlang::caller_env()
)
}
isOldModule <- function(func) {
stopifnot(is.function(func))
required <- c("input", "output", "session")
declared <- names(formals(func))
setequal(required, intersect(required, declared))
} }
#' @noRd #' @noRd
#' @importFrom withr with_options .testModule <- function(module, quosure, dots, env) {
.testModule <- function(module, expr, ...) {
# Capture the environment from the module
# Inserts `session$env <- environment()` at the top of the function
body(module) <- rlang::expr({
session$env <- environment()
!!!body(module)
})
# Create a mock session
session <- MockShinySession$new() session <- MockShinySession$new()
on.exit(if (!session$isClosed()) session$close())
# Parse the additional arguments if (isOldModule(module)) {
args <- rlang::list2(..., input = session$input, output = session$output, session = session) module <- patchModuleFunction(module)
args <- append(dots, list(input = session$input, output = session$output, session = session))
} else {
args <- dots
}
# Initialize the module isolate(withReactiveDomain(session, do.call(module, args)))
isolate(
withReactiveDomain(
session,
withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
# Remember that invoking this module implicitly assigns to `session$env`
# Also, assigning to `$returned` will cause a flush to happen automatically.
session$returned <- do.call(module, args)
})
)
)
# Run the test expression in a reactive context and in the module's environment.
# We don't need to flush before entering the loop because the first expr that we execute is `{`.
# So we'll already flush before we get to the good stuff.
isolate({ isolate({
withReactiveDomain( withReactiveDomain(
session, session,
withr::with_options(list(`shiny.allowoutputreads`=TRUE), { withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
eval(expr, new.env(parent=session$env)) rlang::eval_tidy(
quosure,
data = rlang::as_data_mask(as.list(session$env)),
env = env
)
}) })
) )
}) })
if (!session$isClosed()){
session$close()
}
} }
#' Test an app's server-side logic #' Test an app's server-side logic
@@ -130,8 +125,14 @@ testServer <- function(expr, appDir=NULL) {
formals(server) <- fn_formals formals(server) <- fn_formals
} }
# Now test the server as we would a module # Test the server function almost as if it were a module. `dots` is empty
.testModule(server, expr=substitute(expr)) # because server functions never take additional arguments.
.testModule(
server,
quosure = rlang::enquo(expr),
dots = list(),
env = rlang::caller_env()
)
} }
findApp <- function(startDir="."){ findApp <- function(startDir="."){

View File

@@ -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:

View File

@@ -12,7 +12,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var exports = window.Shiny = window.Shiny || {}; var exports = window.Shiny = window.Shiny || {};
exports.version = "1.4.0.9001"; // Version number inserted by Grunt exports.version = "1.4.0.9002"; // Version number inserted by Grunt
var origPushState = window.history.pushState; var origPushState = window.history.pushState;
window.history.pushState = function () { window.history.pushState = function () {

File diff suppressed because one or more lines are too long

View File

@@ -2,13 +2,18 @@
\alias{HTML} \alias{HTML}
\title{Mark Characters as HTML} \title{Mark Characters as HTML}
\usage{ \usage{
HTML(text, ...) HTML(text, ..., .noWS = NULL)
} }
\arguments{ \arguments{
\item{text}{The text value to mark with HTML} \item{text}{The text value to mark with HTML}
\item{...}{Any additional values to be converted to character and \item{...}{Any additional values to be converted to character and
concatenated together} concatenated together}
\item{.noWS}{Character vector used to omit some of the whitespace that would
normally be written around this HTML. Valid options include \code{before},
\code{after}, and \code{outside} (equivalent to \code{before} and
\code{end}).}
} }
\value{ \value{
The same value, but marked as HTML. The same value, but marked as HTML.

View File

@@ -618,10 +618,18 @@ Trigger a reactive flush right now.
\if{html}{\out{<hr>}} \if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-makeScope"></a>}} \if{html}{\out{<a id="method-makeScope"></a>}}
\subsection{Method \code{makeScope()}}{ \subsection{Method \code{makeScope()}}{
Create and return a namespace-specific session proxy.
\subsection{Usage}{ \subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{MockShinySession$makeScope(namespace)}\if{html}{\out{</div>}} \if{html}{\out{<div class="r">}}\preformatted{MockShinySession$makeScope(namespace)}\if{html}{\out{</div>}}
} }
\subsection{Arguments}{
\if{html}{\out{<div class="arguments">}}
\describe{
\item{\code{namespace}}{Character vector indicating a namespace.}
}
\if{html}{\out{</div>}}
}
} }
\if{html}{\out{<hr>}} \if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-clone"></a>}} \if{html}{\out{<a id="method-clone"></a>}}

View File

@@ -25,6 +25,9 @@ arguments are processed with \code{\link[rlang:list2]{rlang::list2()}} and so ar
will work up the directory hierarchy --- starting with the current directory --- will work up the directory hierarchy --- starting with the current directory ---
looking for a directory that contains an \code{app.R} or \code{server.R} file.} looking for a directory that contains an \code{app.R} or \code{server.R} file.}
} }
\value{
The result of evaluating \code{expr}.
}
\description{ \description{
Offer a way to test the reactive interactions in Shiny --- either in Shiny Offer a way to test the reactive interactions in Shiny --- either in Shiny
modules or in the server portion of a Shiny application. For more modules or in the server portion of a Shiny application. For more

View File

@@ -29,6 +29,7 @@ If the number has a suffix, it must be valid: \code{px},
\code{vmax}. \code{vmax}.
If the number has no suffix, the suffix \code{"px"} is appended. If the number has no suffix, the suffix \code{"px"} is appended.
Any other value will cause an error to be thrown. Any other value will cause an error to be thrown.
} }
\examples{ \examples{

View File

@@ -525,6 +525,33 @@ test_that("testModule works with nested modules", {
}) })
}) })
test_that("testModule calls can be nested", {
outerModule <- function(input, output, session) {
doubled <- reactive({ input$x * 2 })
innerModule <- function(input, output, session) {
quadrupled <- reactive({ doubled() * 2 })
}
}
testModule(outerModule, {
session$setInputs(x = 1)
expect_equal(doubled(), 2)
testModule(innerModule, {
expect_equal(quadrupled(), 4)
})
})
})
test_that("testModule returns a meaningful result", {
result <- testModule(function(input, output, session) {
reactive({ input$x * 2 })
}, {
session$setInputs(x = 2)
session$returned()
})
expect_equal(result, 4)
})
test_that("assigning an output in a module function with a non-function errors", { test_that("assigning an output in a module function with a non-function errors", {
module <- function(input, output, session) { module <- function(input, output, session) {
output$someVar <- 123 output$someVar <- 123
@@ -556,10 +583,43 @@ test_that("testServer works", {
test_that("testServer works when referencing external globals", { test_that("testServer works when referencing external globals", {
# If global is defined at the top of app.R outside of the server function. # If global is defined at the top of app.R outside of the server function.
testServer({ testServer({
expect_equal(global, 123) expect_equal(get("global", session$env), 123)
}, appDir=test_path("..", "test-modules", "06_tabsets")) }, appDir=test_path("..", "test-modules", "06_tabsets"))
}) })
test_that("testModule allows lexical environment access through session$env", {
m <- local({
a_var <- 123
function(input, output, session) {
b_var <- 321
}
})
expect_false(exists("a_var", inherits = FALSE))
testModule(m, {
expect_equal(b_var, 321)
expect_equal(get("a_var", session$env), 123)
})
})
test_that("Module shadowing can be mitigated with unquote", {
i <- 0
inc <- function() i <<- i+1
m <- local({
function(input, output, session) {
inc <- function() stop("I should never be called")
}
})
testModule(m, {
expect_is(inc, "function")
expect_false(identical(inc, !!inc))
!!inc()
})
expect_equal(i, 1)
})
test_that("testModule handles invalidateLater", { test_that("testModule handles invalidateLater", {
module <- function(input, output, session) { module <- function(input, output, session) {
rv <- reactiveValues(x = 0) rv <- reactiveValues(x = 0)

View 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")
})

View File

@@ -72,11 +72,11 @@ test_that("mockableTimer works", {
}) })
test_that("getDomainTimeMs works", { test_that("getDomainTimeMs works", {
start <- Sys.time() start <- as.numeric(Sys.time()) * 1000
t1 <- getDomainTimeMs(NULL) t1 <- getDomainTimeMs(NULL)
t2 <- getDomainTimeMs(list()) t2 <- getDomainTimeMs(list())
t3 <- getDomainTimeMs(list(.now = function(){456})) t3 <- getDomainTimeMs(list(.now = function(){456}))
end <- Sys.time() end <- as.numeric(Sys.time()) * 1000
expect_gte(t1, start) expect_gte(t1, start)
expect_gte(t2, start) expect_gte(t2, start)

View File

@@ -14,3 +14,17 @@ then
else else
echo "No difference detected; Roxygen docs are current." echo "No difference detected; Roxygen docs are current."
fi fi
# Update htmltools docs
Rscript './tools/updateHtmltoolsMan.R'
if [ -n "$(git status --porcelain)" ]
then
git status --porcelain
>&2 echo "Please generate the htmltools documentation and commit the updates."
>&2 echo "The above files changed when we generated the htmltools documentation. This most often occurs when the documentation re-exported by shiny does not match the htmltools documentation."
exit 1
else
echo "No difference detected; re-exported htmltools docs are current."
fi