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 309: Test flexdashboard tab and page management with {bslib} #155

Merged
merged 12 commits into from
May 9, 2023
6 changes: 3 additions & 3 deletions R/data-apps-deps.R
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ apps_deps_map <- list(`001-hello` = "rsconnect", `012-datatables` = "ggplot2",
"rversions", "sf", "withr"), `302-bootswatch-themes` = c("ggplot2",
"progress", "rversions", "sf", "withr"), `304-bslib-card` = c("rlang",
"rversions"), `305-bslib-value-box` = c("rlang", "rversions"
), `310-bslib-sidebar-dynamic` = c("rversions", "testthat"
), `311-bslib-sidebar-toggle-methods` = c("rversions", "testthat"
))
), `309-flexdashboard-tabs-navs` = "rmarkdown", `310-bslib-sidebar-dynamic` = c("rversions",
"testthat"), `311-bslib-sidebar-toggle-methods` = c("rversions",
"testthat"))
73 changes: 73 additions & 0 deletions inst/apps/309-flexdashboard-tabs-navs/index.Rmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: 309
output:
flexdashboard::flex_dashboard:
theme:
version: 3 #<< set to 3, 4, 5, ...
orientation: row
resize_reload: false
params:
bs_version: ""
runtime: shiny
---

gadenbuie marked this conversation as resolved.
Show resolved Hide resolved
Page 1 {data-test-id="Page 1"}
===================================

## Box 1-1 {.tabset data-test-id="Box 1-1"}

### Tab 1-1a {data-test-id="Tab 1-1a"}

**About this test**: This app tests our usage of Bootstrap's Tab plugin.
Test this app in all major versions of Bootstrap.

1. Change the active tabs on this page.
2. Switch to Page 2 under "Other"
3. Switch to Page 3 under "Other"

Verify that only the expected pages are visible
and that the active navbar menu state is correctly shown.

This is Bootstrap `r params$bs_version`.

### Tab 1-1b {data-test-id="Tab 1-1b"}

Tab content, page 1, box 1, tab b

## Box 1-2 {.tabset data-test-id="Box 1-2"}

### Tab 1-2a {data-test-id="Tab 1-2a"}

Tab content, page 1, box 2, tab a

### Tab 1-2b {data-test-id="Tab 1-2b"}

Tab content, page 1, box 2, tab b


Page 2 {data-navmenu="Other" data-test-id="Page 2"}
===================================

## Row

### Box 2-1 {data-test-id="Box 2-1"}

Content, page 2, box 1

### Box 2-2 {data-test-id="Box 2-2"}

Content, page 2, box 2


Page 3 {data-navmenu="Other" data-test-id="Page 3"}
===================================

## Row

### Box 3-1 {data-test-id="Box 3-1"}

Content, page 3, box 1

### Box 3-2 {data-test-id="Box 3-2"}

Content, page 3, box 2
1 change: 1 addition & 0 deletions inst/apps/309-flexdashboard-tabs-navs/tests/testthat.R
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shinytest2::test_app()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Load application support files into testing environment
shinytest2::load_app_env()
118 changes: 118 additions & 0 deletions inst/apps/309-flexdashboard-tabs-navs/tests/testthat/test-shinytest2.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
library(shinytest2)

is_element_visible <- function(selector) {
sprintf("$('%s:visible').length > 0", selector)
}

is_test_element_visible <- function(test_id) {
is_element_visible(sprintf('[data-test-id="%s"]', test_id))
}

expect_test_element_visible <- function(app, test_id) {
expect_true(app$get_js(is_test_element_visible(!!test_id)))
return(invisible(app))
}

expect_test_element_hidden <- function(app, test_id) {
expect_false(app$get_js(is_test_element_visible(!!test_id)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these tests are essentially purely client-side, it'd probably faster/better to use {shinyjster} instead of {shinytest2}, for example:

jster <- shinyjster::shinyjster_js(
"
var jst = jster(0);
jst.add(Jster.shiny.waitUntilStable);
jst.add(function() {
var toggle = $('.navbar-toggle:visible');
var nav = $('.navbar-collapse:visible');
Jster.assert.isEqual(toggle.length, 1, 'Failed to find collapsible menu, does the window need to be resized?');
Jster.assert.isEqual(nav.length, 0, 'The collapsible navbar should not be visible by default');
toggle.click();
});
// wait for nav to open
jst.add(function(done) {
var wait = function() {
if ($('.navbar-collapse:visible').length > 0) {
done();
} else {
setTimeout(wait, 5);
}
}
wait();
});
jst.add(function() {
Jster.assert.isEqual(
$('.navbar-collapse:visible').length, 1,
'Clicking the navbar toggle should make the navbar appear.'
);
});
jst.test();
"
)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schloerke Is shinyjster designed to facilitate running and testing R Markdown Shiny apps using runtime: shiny? (I couldn't find anything in the docs.)

It seems like the biggest advantage of using shinyjster would be to test in Chrome, Edge, and Firefox. From looking at the logs of previous runs, though, and unless I'm misunderstanding them, I would expect switching to shinyjster to increase the test time for these tests. Right now, they take ~7 seconds locally on my M1 for all three Bootstrap variants. With shinyjster we'd be adding two more browsers into the matrix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{shinyjster} can run inside Rmd docs. Ex:

```{r}
## `{shinyjster}` note:
# From https://github.com/rstudio/shiny/issues/3780, we must delay the underlying initial
# call to `Shiny.setInputValue("jster_initialized", true)` due to changes in https://github.com/rstudio/shiny/pull/3666.
# Current stance is that https://github.com/rstudio/shiny/issues/3780 will not be resolved, so we must make a work around.
# This is done by delaying the initial call to `Shiny.setInputValue("jster_initialized", true)`
# by using a dynamic UI that is invalidated on the first draw, and then actually rendered on the second draw.
renderUI({
shinyjster::shinyjster_js(
"
var jst = jster();
jst.add(Jster.shiny.waitUntilStable);
jst.add(function(done) {
var wait = function() {
var txt = $('#status').get(0).textContent;
if (
typeof txt == 'string' &&
txt.length > 0 &&
(txt.match(new RegExp('Pass|Fail')) ?? '').length > 0
) {
done();
return;
}
setTimeout(wait, 100);
}
wait();
})
jst.add(function() {
Jster.assert.isEqual(
$('#status').text().trim(),
'Pass'
)
})
jst.test();
"
)
})
shinyjster::shinyjster_server(input, output)
```

Yes, it's reasonable that the time would be increased, but it is not by a large factor. I'd be comfortable using shinyjster.

return(invisible(app))
}

for (bs_version in 3:5) {
test_that(paste0("309-flexdashboard-tabs-navs with BS", bs_version), {
app <- AppDriver$new(
name = "309-flexdashboard-tabs-navs",
seed = 62868,
height = 1292,
width = 798,
view = interactive(),
render_args = list(
params = list(bs_version = bs_version),
output_options = list(theme = list(version = bs_version))
)
)

app$wait_for_idle()
app$wait_for_js(is_test_element_visible("Page 1"))

# ---- Page 1 ----
# Page 1 and its boxes are visible
expect_test_element_visible(app, "Page 1")
expect_test_element_visible(app, "Box 1-1")
expect_test_element_visible(app, "Box 1-2")

# Check tab state on Page 1 (first tabs are visible)
expect_test_element_visible(app, "Tab 1-1a")
expect_test_element_visible(app, "Tab 1-2a")
# second tabs are hidden
expect_test_element_hidden(app, "Tab 1-1b")
expect_test_element_hidden(app, "Tab 1-2b")

# Pages 2 and 3 and their elements are hidden
expect_test_element_hidden(app, "Page 2")
expect_test_element_hidden(app, "Box 2-1")
expect_test_element_hidden(app, "Box 2-2")
expect_test_element_hidden(app, "Page 3")
expect_test_element_hidden(app, "Box 3-1")
expect_test_element_hidden(app, "Box 3-2")

# ---- Page 1: Change Tabs ----
# activate second tabs and check that visibility has switched
app$
click(selector = '[data-test-id="Page 1"] .nav-tabs [href$="tab-1-1b"]')$
wait_for_js(is_test_element_visible("Tab 1-1b"))

app$
click(selector = '[data-test-id="Page 1"] .nav-tabs [href$="tab-1-2b"]')$
wait_for_js(is_test_element_visible("Tab 1-2b"))

# now first tabs are hidden
expect_test_element_hidden(app, "Tab 1-1a")
expect_test_element_hidden(app, "Tab 1-2a")
# second tabs are visible
expect_test_element_visible(app, "Tab 1-1b")
expect_test_element_visible(app, "Tab 1-2b")

# ---- Page 2 ----
app$
click(selector = ".nav .dropdown .dropdown-toggle")$
wait_for_js(is_element_visible(".nav .dropdown .dropdown-menu"))$
click(selector = '.nav .dropdown-item[href$="page-2"]')$
wait_for_js(is_test_element_visible("Page 2"))

# Page 2 is visible
expect_test_element_visible(app, "Page 2")
expect_test_element_visible(app, "Box 2-1")
expect_test_element_visible(app, "Box 2-2")

# Pages 1 and 3 and their elements are hidden
expect_test_element_hidden(app, "Page 1")
expect_test_element_hidden(app, "Box 1-1")
expect_test_element_hidden(app, "Box 1-2")
expect_test_element_hidden(app, "Page 3")
expect_test_element_hidden(app, "Box 3-1")
expect_test_element_hidden(app, "Box 3-2")

# ---- Page 3 ----
app$
click(selector = ".nav .dropdown .dropdown-toggle")$
wait_for_js(is_element_visible(".nav .dropdown .dropdown-menu"))$
click(selector = '.nav .dropdown-item[href$="page-3"]')$
wait_for_js(is_test_element_visible("Page 3"))

# Page 3 is visible
expect_test_element_visible(app, "Page 3")
expect_test_element_visible(app, "Box 3-1")
expect_test_element_visible(app, "Box 3-2")

# Pages 1 and 2 and their elements are hidden
expect_test_element_hidden(app, "Page 1")
expect_test_element_hidden(app, "Box 1-1")
expect_test_element_hidden(app, "Box 1-2")
expect_test_element_hidden(app, "Page 2")
expect_test_element_hidden(app, "Box 2-1")
expect_test_element_hidden(app, "Box 2-2")

app$stop()
})
}