rstudio / shinytest

Automated testing for shiny apps
https://rstudio.github.io/shinytest/
Other
225 stars 55 forks source link

Custom Shiny input doesn't work with shinytest #426

Open StatisMike opened 2 years ago

StatisMike commented 2 years ago

There seems to be some kind of problem with detecting custom shiny inputs with shinytest.

likertRadioButtons rendered in App, during html inspection have correct css classes (mainly shiny-bound-input, which isn't added during R function UI rendering, so I guess it is appended automatically by Shiny JS during ShinyApp initialization.

<table id="yaml_module-test8" class="form-group shiny-input-likert-radiobuttons shiny-input-container shiny-bound-input" style="width:500px;">`

I guess I created all necessary bindings for the custom input:

// create a binding object
var likertRadioButtonsBinding = new Shiny.InputBinding();

// add methods to it using jQuery's extend method
$.extend(likertRadioButtonsBinding, {

  find: function(scope) {
    // find all instances of class
    return $(scope).find(".shiny-input-likert-radiobuttons");
  },
  initialize: function(el) {
    // bind function onchange to update indicator

    var indicators = document.getElementById(Shiny.$escape(el.id)).
    getElementsByClassName('likert-input-radio indicator-updater')
    // only if the indicator is there!

    if (indicators.length != 0) {
      $(indicators).change(function(){
         var text=$(this).attr('choice-name');
         var id=$(this).attr('name');
         document.getElementById(id).getElementsByClassName('likert-input-radio-indicator')[0].textContent=text;
       });
    };
  },
  getValue(el) {
    // Select the radio objects that have name equal to the grouping div's id
    const checkedItems = $(
      'input:radio[name="' + Shiny.$escape(el.id) + '"]:checked'
    );

    if (checkedItems.length === 0) {
      return null;
    }

    return Number(checkedItems.val());
  },
  setValue(el, value) {
    if ($.isArray(value) && value.length === 0) {
      // Removing all checked item if the sent data is empty
      $('input:radio[name="' + Shiny.$escape(el.id) + '"]').prop("checked", false);
    } else {
      $(
        'input:radio[name="' +
          Shiny.$escape(el.id) +
          '"][value="' +
          value +
          '"]'
      ).prop("checked", true);
    }
    $(el).trigger("change");
  },
  subscribe(el, callback) {
    $(el).on("change.likertRadioButtonsBinding", function () {
      callback(false);
    });
  },
  unsubscribe(el) {
    $(el).off(".likertRadioButtonsBinding");
  }
});
Shiny.inputBindings.register(likertRadioButtonsBinding);

Should any other actions be done when testing app with custom inputs (like declaring new input bindings directly)? Or maybe the receiveMessage method is crucial for shinytest to work correctly (I haven't created updateLikertRadioButtons and this method in JS yet, as I am not quite good at JS ATM)

Update: creating small mockup shinyApp with two likertRadioButtons input and using browser() I can see that the inputs are declared there. shinytest don't detect them though. How does it detect the inputs? Going through source files it seemed like it takes them directly from input element, but it seems like that isn't complete story.

> runApp('test/likertInputTests')

Listening on http://127.0.0.1:6591
Called from: server(...)
Browse[1]> input
<ReactiveValues> 
  Values:    first, second 
  Readonly:  TRUE 

Update 2: For more in-depth checks, you can install shiny.quetzio and call shiny.quetzio:::testthat_likert_app() for exemplary app with inputs not visible to shinytest during testApp runs.