Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add $click() method to MockSession #2745

Open
alandipert opened this issue Jan 21, 2020 · 8 comments · May be fixed by #3855
Open

Add $click() method to MockSession #2745

alandipert opened this issue Jan 21, 2020 · 8 comments · May be fixed by #3855

Comments

@alandipert
Copy link
Contributor

MockSession exposes a setInputs() method that can be used to programmatically set inputs, but we don't offer an easy way to click an action button programmatically without an understanding of the way button inputs transition from NULL to 0 first click and increase monotonically thereafter.

I propose a new$click() method on MockSession that accepts a character vector of actionButton input names and "clicks" them, internally handling value transitions.

@julianstanley
Copy link

julianstanley commented Jun 22, 2020

I think that testServer also works through a MockSession, right?

If so, then this is might be related: can we trigger clicks from testServer? I guess if we had a session$click() method, that would work--though, I wonder why session$setInputs doesn't work for this (should I post this as a separate issue?)

Setting input$my_button doesn't trigger eventReactive
library(shiny)
example_module_server <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      eventReactive(input$my_button, {
        print("Button was pressed")
      })
    }
  )
}

testServer(example_module_server, {
  # my_button should already be NULL, but for good measure:
  session$setInputs(my_button = NULL)
  # This should be what happens when the button is pressed, right?
  session$setInputs(my_button = 0)
})

I expected some print output. For example, if this were just a reactive:

library(shiny)
example_module_server <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      myfun <- reactive({
        print("Function was called")
      })
    }
  )
}

testServer(example_module_server, {
  myfun()
})
[1] "Function was called"

Edit: @alandipert I would love to try to help with this, if possible, but I don't think I understand this enough yet to get started. You mentioned that you can't click an action button without knowing that buttons start at NULL and then transition to 0, but in the example above, it seems like setting a button from NULL to 0 does not trigger an eventReactive. Do you know why that is?

Edit2: This is just because I was using an eventReactive, and nothing updated. Sorry for the confusion, I should've used observeEvent.

@dmenne
Copy link
Contributor

dmenne commented Feb 5, 2022

Is there any solution to this? How do I simulate a click?

@gadenbuie
Copy link
Member

Using the example from @julianstanley, this is how I would recommend testing a button "click" programmatically. For the first click, set the input to 0 (or any number, really). For clicks thereafter, set the input to <input> + 1.

library(shiny)

example_module_server <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      my_reactive <- reactive(paste("Button was pressed:", input$my_button))
      
      observe(message(my_reactive()))
    }
  )
}

testServer(example_module_server, {
  # First programmatic 'click'
  session$setInputs(my_button = 0)
  stopifnot(my_reactive() == "Button was pressed: 0")
  
  # Second programmatic 'click'
  session$setInputs(my_button = input$my_button + 1)
  stopifnot(my_reactive() == "Button was pressed: 1")
})
#> Button was pressed: 0
#> Button was pressed: 1

@daattali
Copy link
Contributor

Since #3764 was closed as "not planned", does it mean this will also not be implemented? I'm fully aware of the workaround, but it still seems like a very incovenient way of doing a simple action.

@Shelmith-Kariuki
Copy link

Hi @gadenbuie ,

Thank you for your assistance. I'm still not able to sort my issue out. I don't understand why the code below fails with the error: Error in displayed_text_r() : could not find function "displayed_text_r"

#' kwanza UI Function
#'
#' @description A shiny Module that contains code for the first tab.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @noRd
#'
#' @importFrom shiny NS tagList
mod_kwanza_ui <- function(id){
  ns <- NS(id)
  shiny::tagList(
    shiny::fluidPage(
      shiny::textInput(ns("firstname"), "First name", value = NULL),
      shiny::br(),
      shiny::textInput(ns("secondname"), "Second name", value = NULL),
      shiny::br(),
      shiny::numericInput(ns("yob"), "Year of birth", value = NULL),
      shiny::br(),
      shiny::actionButton(ns("go_button"), "Go"),
      shiny::br(),
      shiny::br(),
      shiny::textOutput(ns("displayed_text")),
      shiny::br(),
      shiny::br(),
      shiny::actionButton(ns("kwanza_next"), "Next"),
    )


  )
}

#' kwanza Server Functions
#'
#' @noRd
mod_kwanza_server <- function(id){
  moduleServer( id, function(input, output, session){
    ns <- session$ns

    shiny::observeEvent(input$go_button, {
    ## activating this observeEvent makes the test fail with the error
    ##`Error in displayed_text_r() : could not find function "displayed_text_r"`

    displayed_text_r <- shiny::reactive({
      paste0("Hi ", input$firstname , " ", input$secondname ," ! " ,
             "You are ", ceiling(lubridate::year(Sys.Date())- input$yob), " years old.")
    })

    output$displayed_text <- shiny::renderText({
      displayed_text_r()
    })

    })

  })
}

## Comment these lines if you want to view the app.
# # Shiny App
# ui <- fluidPage(
#   mod_kwanza_ui("kwanza_1")
# )
#
# server <- function(input, output, session) {
#   mod_kwanza_server("kwanza_1")
# }
#
# # Run the Shiny App
# shinyApp(ui, server)




# test function
testServer(
  mod_kwanza_server,
  # Add here your module params
  args = list()
  , {
    ns <- session$ns

    session$setInputs(firstname = "Shel")
    session$setInputs(secondname = "Kariuki")
    session$setInputs(yob = 2000)

    session$setInputs(go_button = 0) ## Is this right?

    print(displayed_text_r()) #for my own sanity

    testthat::expect_true(grepl( "Shel", displayed_text_r()))

  })


@gadenbuie
Copy link
Member

gadenbuie commented Jun 28, 2023

Since #3764 was closed as "not planned", does it mean this will also not be implemented? I'm fully aware of the workaround, but it still seems like a very incovenient way of doing a simple action.

I think that's a reasonable conclusion. First: I recognize the friction identified and experienced around testing action buttons. I think we should do something to alleviate that friction and I propose we update the documentation of ?testServer to cover a more real-world example or to specifically call out the testing of action buttons.

I understand the draw of wanting to solve this with a dead-simple helper method like $click(), but the simplicity of its implementation hides many other considerations. $setInputs() is a generic input-setting function that is guaranteed to work for all inputs, although it requires you to understand a bit about how those inputs report their values to the server. This generalizability and consistency is important for testing infrastructure. Adding input-specific setting functions increase the maintenance burden of this area of Shiny, one that's already somewhat distant from the actual implementation of the inputs in the first place.

Furthermore, $click() is likely to create more confusion than it solves. As described here, "click" is specific to clicking only on an actionButton(). But users may understandably expect $click() to simulate a click on other areas of their application UI, which will not work unless we implement other input-specific logic. We could rename the method setActionButtonInput() or something similar, which further highlights my first point about it being isolated to a single input.

Summarizing from my previous comment in #3746, I think its reasonable for users of testServer() who are likely testing modules to be far enough along in their Shiny journey to be ready to learn a little bit about how action buttons operate on the server side.

@gadenbuie
Copy link
Member

Hi @Shelmith-Kariuki, thanks for sharing your app code. I think the problem with your module is that you define the displayed_text_r reactive inside the observeEvent() call. This means that it doesn't exist in a context where testServer() can call it. I'd recommend restructuring your app so that the reactive and the output are set outside the observeEvent().

Here's a smaller version that uses bindEvent() to bind the reactive updating to the button click:

library(shiny)

example_module_server <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      name <- reactive({
        paste(input$first, input$last)
      }) |>
        bindEvent(input$button)
    }
  )
}

testServer(example_module_server, {
  session$setInputs(first = "John", last = "Doe")
  # click on the button (important because otherwise calling name() errors)
  session$setInputs(button = 1)
  stopifnot(name() == "John Doe")
  
  session$setInputs(first = "Jane")
  # The name() reactive should not have changed yet
  stopifnot(name() == "John Doe")
  
  session$setInputs(button = 2)
  stopifnot(name() == "Jane Doe")
})

@Shelmith-Kariuki
Copy link

Hi @gadenbuie

Thank you so much. That works.

cc @arthur-shaw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants