# toast() constructor tests ---- test_that("toast() creates bslib_toast object with defaults", { t <- toast("Test message") expect_s3_class(t, "bslib_toast") expect_null(t$id) expect_true(t$autohide) expect_equal(t$duration, 5000) # Default 5 seconds in milliseconds expect_true(t$closable) expect_null(t$header) expect_null(t$icon) expect_equal(t$position, "top-right") }) test_that("toast() validates position argument", { expect_no_error(toast("Test", position = "bottom-left")) expect_no_error(toast("Test", position = "top-center")) expect_no_error(toast("Test", position = "middle-center")) expect_snapshot(error = TRUE, toast("Test", position = "invalid")) }) test_that("toast() validates type argument", { expect_no_error(toast("Test", type = "success")) expect_no_error(toast("Test", type = "danger")) expect_no_error(toast("Test", type = "info")) expect_snapshot(error = TRUE, toast("Test", type = "invalid")) }) test_that("toast() type 'error' is aliased to 'danger'", { t <- toast("Test", type = "error") expect_equal(t$type, "danger") }) test_that("toast() autohide disabled (0, NA, NULL)", { # When autohide is disabled, closable can be set to FALSE # This allows app authors to manage toast display manually t1 <- toast("Test", duration_s = 0, closable = FALSE) expect_false(t1$autohide) expect_false(t1$closable) t2 <- toast("Test", duration_s = NA, closable = FALSE) expect_false(t2$autohide) expect_false(t2$closable) t3 <- toast("Test", duration_s = NULL, closable = FALSE) expect_false(t3$autohide) expect_false(t3$closable) # closable can also be TRUE when autohide is disabled t4 <- toast("Test", duration_s = NA, closable = TRUE) expect_false(t4$autohide) expect_true(t4$closable) }) test_that("toast() `closable` when autohide enabled", { # When autohide is enabled, user can control closable t_closable <- toast("Test", duration_s = 10, closable = TRUE) expect_true(t_closable$autohide) expect_equal(t_closable$duration, 10000) # Converted to milliseconds expect_true(t_closable$closable) t_not_closable <- toast("Test", duration_s = 5, closable = FALSE) expect_true(t_not_closable$autohide) expect_equal(t_not_closable$duration, 5000) expect_false(t_not_closable$closable) }) test_that("toast() duration_s throws for invalid values", { expect_snapshot(error = TRUE, { toast("Test", duration_s = -5) toast("Test", duration_s = "invalid") toast("Test", duration_s = c(5, 10)) }) }) test_that("toast() stores icon argument", { icon_elem <- span(class = "test-icon", HTML("★")) t <- toast("Test message", icon = icon_elem) expect_s3_class(t, "bslib_toast") expect_s3_class(t$icon, "shiny.tag") expect_equal(t$icon$attribs$class, "test-icon") expect_equal(t$icon$children[[1]], HTML("★")) }) test_that("toast() icon is NULL by default", { t <- toast("Test message") expect_null(t$icon) }) # toast() rendering tests ---- test_that("as.tags.bslib_toast creates proper HTML structure", { t <- toast( body = "Test message", header = "Test", type = "success", id = "test-toast" ) tag <- as.tags(t) expect_s3_class(tag, "shiny.tag") expect_snapshot(cat(format(tag))) }) test_that("as.tags.bslib_toast generates ID if not provided", { t <- toast("Test message") tag <- as.tags(t) html_str <- as.character(tag) # Verify an auto-generated ID is present expect_match(html_str, 'id="bslib-toast-[0-9]+"') }) test_that("as.tags.bslib_toast respects accessibility attributes", { # Danger type gets assertive role t_danger <- toast("Error message", type = "danger", id = "danger-toast") html_danger <- as.character(as.tags(t_danger)) expect_match(html_danger, 'role="alert"') expect_match(html_danger, 'aria-live="assertive"') expect_snapshot(cat(format(as.tags(t_danger)))) # Info type gets polite role t_info <- toast("Info message", type = "info", id = "info-toast") html_info <- as.character(as.tags(t_info)) expect_match(html_info, 'role="status"') expect_match(html_info, 'aria-live="polite"') expect_snapshot(cat(format(as.tags(t_info)))) # NULL type (default) gets polite role t_default <- toast("Default message", id = "default-toast") html_default <- as.character(as.tags(t_default)) expect_match(html_default, 'role="status"') expect_match(html_default, 'aria-live="polite"') expect_snapshot(cat(format(as.tags(t_default)))) }) test_that("as.tags.bslib_toast includes close button appropriately", { # With header, closable t_header <- toast( "Message", header = "Title", closable = TRUE, id = "header-toast" ) expect_snapshot(cat(format(as.tags(t_header)))) # Without header, closable t_no_header <- toast("Message", closable = TRUE, id = "no-header-toast") expect_snapshot(cat(format(as.tags(t_no_header)))) # Non-closable with autohide t_non_closable <- toast( "Message", closable = FALSE, duration_s = 5, id = "non-closable-toast" ) expect_snapshot(cat(format(as.tags(t_non_closable)))) # Non-closable with autohide disabled (for manual management) t_manual <- toast( "Message", closable = FALSE, duration_s = NA, id = "manual-toast" ) expect_snapshot(cat(format(as.tags(t_manual)))) }) test_that("toast() icon renders in body without header", { icon_elem <- span(class = "my-icon", HTML("★")) t <- toast("You have new messages", icon = icon_elem, id = "icon-toast") tag <- as.tags(t) html <- as.character(tag) # Icon should be in toast-body with special wrapper expect_match(html, 'class="toast-body d-flex gap-2"') expect_match(html, 'class="toast-body-icon"') expect_match(html, 'class="my-icon"') expect_match(html, "★") expect_match(html, 'class="toast-body-content flex-grow-1"') expect_snapshot(cat(format(tag))) }) test_that("toast() icon renders in body with header", { icon_elem <- span(class = "header-icon", HTML("★")) t <- toast( "Message content", header = "New Mail", icon = icon_elem, id = "icon-header-toast" ) tag <- as.tags(t) html <- as.character(tag) # Icon should still be in body when header is present expect_match(html, 'class="toast-body d-flex gap-2"') expect_match(html, 'class="toast-body-icon"') expect_match(html, 'class="header-icon"') expect_match(html, "★") expect_snapshot(cat(format(tag))) }) test_that("toast() icon works with closable button in body", { icon_elem <- span(class = "alert-icon", HTML("★")) t <- toast( "Warning message", icon = icon_elem, closable = TRUE, id = "icon-closable-toast" ) tag <- as.tags(t) html <- as.character(tag) # Should have both icon and close button in body expect_match(html, 'class="toast-body d-flex gap-2"') expect_match(html, 'class="toast-body-icon"') expect_match(html, 'class="alert-icon"') expect_match(html, "★") expect_match(html, 'class="btn-close"') expect_snapshot(cat(format(tag))) }) test_that("toast() without icon or close button has simple body", { t <- toast( "Simple message", header = "Header", closable = FALSE, id = "simple-body-toast" ) tag <- as.tags(t) html <- as.character(tag) # Should have simple toast-body (no d-flex gap-2) expect_match(html, 'class="toast-body"') expect_false(grepl('class="toast-body d-flex gap-2"', html)) expect_false(grepl('toast-body-icon', html)) expect_false(grepl('toast-body-content', html)) }) # toast_header() tests ---- test_that("toast_header() creates structured header data", { # Simple header with just title h1 <- toast_header("My Title") expect_s3_class(h1, "bslib_toast_header") expect_equal(as.character(h1$title), "My Title") expect_null(h1$icon) expect_null(h1$status) # Header with status text h2 <- toast_header("Success", status = "11 mins ago") expect_s3_class(h2, "bslib_toast_header") expect_equal(as.character(h2$title), "Success") expect_equal(h2$status, "11 mins ago") }) test_that("toast_header() works with icons", { icon <- span(class = "test-icon") h <- toast_header("Title", icon = icon) expect_s3_class(h, "bslib_toast_header") expect_equal(as.character(h$title), "Title") expect_s3_class(h$icon, "shiny.tag") expect_equal(h$icon$attribs$class, "test-icon") }) test_that("toast_header() icon renders in header", { icon_elem <- span(class = "header-test-icon", HTML("★")) h <- toast_header("Notification", icon = icon_elem, status = "now") t <- toast("Body content", header = h, id = "header-icon-toast") tag <- as.tags(t) html <- as.character(tag) # Icon should be in toast-header with wrapper expect_match(html, 'class="toast-header"') expect_match(html, 'class="toast-header-icon"') expect_match(html, 'class="header-test-icon"') expect_match(html, "★") expect_snapshot(cat(format(tag))) }) test_that("toast_header() icon with status and title", { icon_elem <- span(class = "success-icon", "✓") h <- toast_header("Success", icon = icon_elem, status = "just now") t <- toast("Operation completed", header = h, id = "full-header-toast") tag <- as.tags(t) html <- as.character(tag) # Should have all three elements: icon, title, status expect_match(html, 'class="toast-header-icon"') expect_match(html, 'class="success-icon"') expect_match(html, "✓") expect_match(html, "Success") expect_match(html, "just now") expect_match(html, 'class="text-muted text-end"') expect_snapshot(cat(format(tag))) }) test_that("toast() stores additional attributes", { t <- toast("Test", `data-test` = "value", class = "extra-class") expect_equal(t$attribs$`data-test`, "value") expect_equal(t$attribs$class, "extra-class") tag <- as.tags(t) html <- as.character(tag) expect_true(grepl('data-test="value"', html)) expect_true(grepl('class="toast[^"]+extra-class"', html)) }) test_that("toast() type is reflected in rendered HTML", { t_success <- toast("Test", type = "success") expect_equal(t_success$type, "success") tag_success <- as.tags(t_success) html_success <- as.character(tag_success) expect_true(grepl("text-bg-success", html_success)) t_danger <- toast("Test", type = "danger") expect_equal(t_danger$type, "danger") tag_danger <- as.tags(t_danger) html_danger <- as.character(tag_danger) expect_true(grepl("text-bg-danger", html_danger)) }) test_that("toast() position is stored correctly", { t1 <- toast("Test", position = "top-left") expect_equal(t1$position, "top-left") t2 <- toast("Test", position = "middle-center") expect_equal(t2$position, "middle-center") t3 <- toast("Test", position = "bottom-right") expect_equal(t3$position, "bottom-right") }) # toast() header integration tests ---- test_that("toast header with character", { t <- toast("Body", header = "Simple Header") tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("toast-header", html)) expect_true(grepl("Simple Header", html)) expect_true(grepl("me-auto", html)) }) test_that("toast header with toast_header()", { t <- toast( "Body", header = toast_header("Title", status = "just now") ) tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("toast-header", html)) expect_true(grepl("Title", html)) expect_true(grepl("just now", html)) expect_true(grepl("text-muted", html)) }) test_that("toast header with list(title = ...) pattern", { # Bare list with title should work like toast_header() t <- toast( "Body", header = list(title = "Title", status = "just now") ) tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("toast-header", html)) expect_true(grepl("Title", html)) expect_true(grepl("just now", html)) expect_true(grepl("text-muted", html)) }) test_that("toast header with custom tag", { t <- toast( "Body", header = div(class = "custom-header", "My Header") ) tag <- as.tags(t) expect_equal(tag$children[[1]]$attribs$class, "toast-header") expect_equal( format(tag$children[[1]]$children[[1]]), '
My Header
' ) }) test_that("toast header can be modified after creation", { # Create toast with toast_header() t <- toast( "Body", header = toast_header("Original Title", status = "1 min ago") ) # Modify the header t$header$title <- "Updated Title" t$header$status <- "just now" tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("Updated Title", html)) expect_true(grepl("just now", html)) expect_false(grepl("Original Title", html)) expect_false(grepl("1 min ago", html)) }) test_that("toast header with icon can be modified after creation", { # Create toast with toast_header() including icon icon1 <- span(class = "icon-1", "A") t <- toast( "Body", header = toast_header("Title", icon = icon1) ) # Modify the header icon icon2 <- span(class = "icon-2", "B") t$header$icon <- icon2 tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("icon-2", html)) expect_true(grepl("B", html, fixed = TRUE)) expect_false(grepl("icon-1", html)) expect_false(grepl("A", html, fixed = TRUE)) }) test_that("toast header with list pattern and icon", { # Bare list with title and icon icon_elem <- span(class = "list-icon", HTML("★")) t <- toast( "Body", header = list( title = "Notes", icon = icon_elem, status = "updated" ) ) tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("toast-header", html)) expect_true(grepl("Notes", html)) expect_true(grepl("updated", html)) expect_true(grepl("list-icon", html)) expect_true(grepl("★", html)) }) test_that("toast header can be replaced with list pattern", { # Create toast with simple character header t <- toast("Body", header = "Simple") # Replace with list pattern t$header <- list( title = "New Title", status = "now", icon = span(class = "icon") ) tag <- as.tags(t) html <- as.character(tag) expect_true(grepl("New Title", html)) expect_true(grepl("now", html)) expect_true(grepl("icon", html)) expect_false(grepl("Simple", html)) }) test_that("toast with both header icon and body icon", { # Both header and body can have their own icons header_icon <- span(class = "h-icon", "H") body_icon <- span(class = "b-icon", "B") t <- toast( "Message content", header = toast_header("Title", icon = header_icon), icon = body_icon, id = "dual-icon-toast" ) tag <- as.tags(t) html <- as.character(tag) # Both icons should be present in different locations expect_match(html, 'class="toast-header-icon"') expect_match(html, 'class="h-icon"') expect_match(html, 'class="toast-body-icon"') expect_match(html, 'class="b-icon"') expect_snapshot(cat(format(tag))) }) # normalize_toast_position() helper tests ---- test_that("normalize_toast_position() handles standard kebab-case format", { expect_equal(normalize_toast_position("top-left"), "top-left") expect_equal(normalize_toast_position("bottom-right"), "bottom-right") expect_equal( normalize_toast_position("middle-center"), "middle-center" ) }) test_that("normalize_toast_position() handles space-separated format", { expect_equal(normalize_toast_position("top left"), "top-left") expect_equal(normalize_toast_position("bottom right"), "bottom-right") expect_equal( normalize_toast_position("middle center"), "middle-center" ) }) test_that("normalize_toast_position() handles reversed order", { expect_equal(normalize_toast_position("left top"), "top-left") expect_equal(normalize_toast_position("right bottom"), "bottom-right") expect_equal( normalize_toast_position("center middle"), "middle-center" ) }) test_that("normalize_toast_position() handles vector input", { expect_equal(normalize_toast_position(c("top", "left")), "top-left") expect_equal( normalize_toast_position(c("bottom", "right")), "bottom-right" ) expect_equal(normalize_toast_position(c("left", "top")), "top-left") expect_equal( normalize_toast_position(c("right", "bottom")), "bottom-right" ) }) test_that("normalize_toast_position() is case-insensitive", { expect_equal(normalize_toast_position("TOP LEFT"), "top-left") expect_equal(normalize_toast_position("Bottom Right"), "bottom-right") expect_equal( normalize_toast_position("MIDDLE center"), "middle-center" ) }) test_that("normalize_toast_position() handles default NULL or empty", { expect_equal(normalize_toast_position(NULL), "bottom-right") expect_equal(normalize_toast_position(character(0)), "bottom-right") }) test_that("normalize_toast_position() defaults to bottom-right when unspecified", { expect_equal(normalize_toast_position(), "bottom-right") }) test_that("normalize_toast_position() handles all valid combinations", { # Space-separated expect_equal(normalize_toast_position("top left"), "top-left") expect_equal(normalize_toast_position("middle center"), "middle-center") expect_equal(normalize_toast_position("bottom right"), "bottom-right") # Reversed order expect_equal(normalize_toast_position("left top"), "top-left") expect_equal(normalize_toast_position("center middle"), "middle-center") expect_equal(normalize_toast_position("right bottom"), "bottom-right") # Vector input expect_equal(normalize_toast_position(c("top", "left")), "top-left") expect_equal(normalize_toast_position(c("center", "middle")), "middle-center") expect_equal(normalize_toast_position(c("bottom", "right")), "bottom-right") }) test_that("normalize_toast_position() errors on invalid input", { expect_snapshot(error = TRUE, { # Missing components normalize_toast_position("top") normalize_toast_position("left") # Duplicate components normalize_toast_position("top bottom left") normalize_toast_position(c("top", "bottom", "left")) # Invalid components normalize_toast_position("top invalid") normalize_toast_position("foo bar") }) }) test_that("normalize_toast_position() handles extra whitespace", { expect_equal(normalize_toast_position(" top left "), "top-left") expect_equal( normalize_toast_position("bottom right"), "bottom-right" ) }) # show_toast() and hide_toast() tests ---- test_that("toast() works with flexible position formats", { # Space-separated t1 <- toast("Test", position = "top left") expect_equal(t1$position, "top-left") # Reversed order t2 <- toast("Test", position = "right bottom") expect_equal(t2$position, "bottom-right") # Vector t3 <- toast("Test", position = c("middle", "center")) expect_equal(t3$position, "middle-center") }) test_that("show_toast() returns the toast id", { local_mocked_bindings( toast_random_id = function() "bslib-toast-1234" ) session <- list(sendCustomMessage = function(type, message) { expect_equal(type, "bslib.show-toast") expect_equal(message$id, !!exp_toast_id) }) t <- toast("Test message") exp_toast_id <- "bslib-toast-1234" toast_id <- show_toast(t, session = session) expect_equal(toast_id, exp_toast_id) exp_toast_id <- "custom-id" t2 <- toast("Another message", id = exp_toast_id) toast_id2 <- show_toast(t2, session = session) expect_equal(toast_id2, exp_toast_id) }) test_that("show_toast() converts string to toast automatically", { local_mocked_bindings( toast_random_id = function() "bslib-toast-auto" ) message_sent <- FALSE session <- list(sendCustomMessage = function(type, message) { expect_equal(type, "bslib.show-toast") expect_equal(message$id, "bslib-toast-auto") message_sent <<- TRUE }) # Pass a plain string instead of a toast object toast_id <- show_toast("Simple message", session = session) expect_true(message_sent) expect_equal(toast_id, "bslib-toast-auto") }) test_that("show_toast() and hide_toast() warn if nothing to show/hide", { session <- list(sendCustomMessage = function(type, message) { stop("sendCustomMessage should not be called") }) expect_snapshot({ hide_toast(show_toast(toast(), session = session), session = session) }) }) test_that("hide_toast() works", { session <- list(sendCustomMessage = function(type, message) { expect_equal(type, "bslib.hide-toast") expect_equal(message$id, !!exp_toast_id) }) exp_toast_id <- "bslib-toast-1234" t_id <- hide_toast(exp_toast_id, session = session) expect_equal(t_id, exp_toast_id) exp_toast_id <- "custom-id" t_id2 <- hide_toast(toast("Test", id = exp_toast_id), session = session) expect_equal(t_id2, exp_toast_id) expect_snapshot(error = TRUE, { hide_toast(toast()) }) })