From b4ee7b732f550f41793d8679ff8500408123e4e6 Mon Sep 17 00:00:00 2001 From: James J Balamuta Date: Thu, 2 Jan 2025 18:21:02 -0800 Subject: [PATCH] Add unit tests (#3) * Add unit tests * Fix description length exceeding 100 characters for binary documentation * Fix example --- DESCRIPTION | 5 +- R/writers.R | 10 +- man/write_file_content.Rd | 10 +- tests/testthat.R | 12 ++ tests/testthat/.gitignore | 1 + tests/testthat/test-find.R | 104 +++++++++++ tests/testthat/test-peek.R | 160 ++++++++++++++++ tests/testthat/test-quarto-cell-parser.R | 83 +++++++++ .../test-shinylive-quarto-apps-commands.R | 107 +++++++++++ .../test-shinylive-standalone-app-commands.R | 96 ++++++++++ tests/testthat/test-utils.R | 59 ++++++ tests/testthat/test-writers.R | 171 ++++++++++++++++++ 12 files changed, 807 insertions(+), 11 deletions(-) create mode 100644 tests/testthat.R create mode 100644 tests/testthat/.gitignore create mode 100644 tests/testthat/test-find.R create mode 100644 tests/testthat/test-peek.R create mode 100644 tests/testthat/test-quarto-cell-parser.R create mode 100644 tests/testthat/test-shinylive-quarto-apps-commands.R create mode 100644 tests/testthat/test-shinylive-standalone-app-commands.R create mode 100644 tests/testthat/test-utils.R create mode 100644 tests/testthat/test-writers.R diff --git a/DESCRIPTION b/DESCRIPTION index 6f1eb04..7086887 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,5 +21,8 @@ RoxygenNote: 7.3.2 Suggests: knitr, rmarkdown, - quarto + quarto, + withr, + testthat (>= 3.0.0) VignetteBuilder: quarto +Config/testthat/edition: 3 diff --git a/R/writers.R b/R/writers.R index 539b6ae..e34a4c2 100644 --- a/R/writers.R +++ b/R/writers.R @@ -39,12 +39,12 @@ #' type = "text" #' ) #' -#' # Writing a binary file (base64-encoded content) -#' write_file_content( -#' content = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", -#' file_path = "app/www/image.png", -#' type = "binary" +#' # Write base64 encoded image +#' b64img <- paste0( +#' "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA", +#' "DUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" #' ) +#' write_file_content(b64img, "test.png", type = "binary") #' } #' #' @keywords internal diff --git a/man/write_file_content.Rd b/man/write_file_content.Rd index 73dbc96..e057cf7 100644 --- a/man/write_file_content.Rd +++ b/man/write_file_content.Rd @@ -54,12 +54,12 @@ write_file_content( type = "text" ) -# Writing a binary file (base64-encoded content) -write_file_content( - content = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", - file_path = "app/www/image.png", - type = "binary" +# Write base64 encoded image +b64img <- paste0( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA", + "DUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" ) +write_file_content(b64img, "test.png", type = "binary") } } diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..a57ac3d --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(peeky) + +test_check("peeky") diff --git a/tests/testthat/.gitignore b/tests/testthat/.gitignore new file mode 100644 index 0000000..09fe695 --- /dev/null +++ b/tests/testthat/.gitignore @@ -0,0 +1 @@ +converted_shiny_app diff --git a/tests/testthat/test-find.R b/tests/testthat/test-find.R new file mode 100644 index 0000000..a69838c --- /dev/null +++ b/tests/testthat/test-find.R @@ -0,0 +1,104 @@ +# Test find_shinylive_app_json() ---- + +test_that("find_shinylive_app_json(): validates JSON structure correctly", { + # Create a valid app.json structure with required fields + valid_json <- list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + + # Create a mock HTTP response with valid JSON content + valid_resp <- base::structure( + list( + headers = list("content-type" = "application/json"), + content = base::charToRaw(jsonlite::toJSON(valid_json)) + ), + class = "response" + ) + + # Mock httr functions to simulate successful API response + testthat::local_mocked_bindings( + GET = function(...) valid_resp, + content = function(...) jsonlite::toJSON(valid_json), + .package = "httr" + ) + + # Test with valid JSON + result <- find_shinylive_app_json("http://example.com/app.json") + + # Verify successful validation + testthat::expect_true(result$valid) + testthat::expect_equal(result$url, "http://example.com/app.json") + testthat::expect_equal(result$data, valid_json) + + # Create a mock response with invalid JSON (empty object) + invalid_resp <- base::structure( + list( + headers = list("content-type" = "application/json"), + content = base::charToRaw("{}") + ), + class = "response" + ) + + # Mock httr functions to simulate invalid JSON response + testthat::local_mocked_bindings( + GET = function(...) invalid_resp, + content = function(...) "{}", + .package = "httr" + ) + + # Test with invalid JSON + result <- find_shinylive_app_json("http://example.com/app.json") + + # Verify failed validation + testthat::expect_false(result$valid) + testthat::expect_null(result$url) + testthat::expect_null(result$data) +}) + +# Test find_shinylive_code() ---- + +test_that("find_shinylive_code(): extracts code blocks correctly", { + # Create HTML content containing both R and Python Shinylive code blocks + # Note the different structures and options in each block + html_content <- ' +
+  #| viewerHeight: 500
+  #| standalone: true
+  ## file: app.R
+  library(shiny)
+  ui <- fluidPage()
+  server <- function(input, output) {}
+  shinyApp(ui, server)
+  
+
+  #| standalone: true
+  ## file: app.py
+  from shiny import App, ui
+  app = App(app_ui)
+  
+ ' + + # Parse the HTML content to find code blocks + result <- find_shinylive_code(html_content) + + # Verify number of code blocks found + testthat::expect_equal(base::length(result), 2) + + # Verify correct engine identification for each block + testthat::expect_equal(result[[1]]$engine, "r") + testthat::expect_equal(result[[2]]$engine, "python") + + # Verify R code block structure and options + testthat::expect_true("viewerHeight" %in% base::names(result[[1]]$options)) + testthat::expect_equal(result[[1]]$options$viewerHeight, 500) + testthat::expect_true("app.R" %in% base::names(result[[1]]$files)) + + # Verify Python code block structure and options + testthat::expect_true("standalone" %in% base::names(result[[2]]$options)) + testthat::expect_true(result[[2]]$options$standalone) + testthat::expect_true("app.py" %in% base::names(result[[2]]$files)) +}) diff --git a/tests/testthat/test-peek.R b/tests/testthat/test-peek.R new file mode 100644 index 0000000..8d74090 --- /dev/null +++ b/tests/testthat/test-peek.R @@ -0,0 +1,160 @@ +# Test peek_shinylive_app() ---- + +test_that("peek_shinylive_app(): handles HTML content correctly", { + # Create sample HTML content with an embedded R Shiny application + # The content simulates a Quarto document structure with a shinylive-r code block + html_content <- '
+
+    #| viewerHeight: 500
+    ## file: app.R
+    library(shiny)
+    ui <- fluidPage()
+    server <- function(input, output) {}
+    shinyApp(ui, server)
+    
+
' + + # Mock HTTP-related functions to simulate web requests + testthat::local_mocked_bindings( + # Mock GET to return HTML content with appropriate headers + GET = function(...) base::structure( + list( + headers = list("content-type" = "text/html"), + content = base::charToRaw(html_content) + ), + class = "response" + ), + # Mock http_error to always return FALSE (success) + http_error = function(...) FALSE, + # Mock content function to return the HTML content + content = function(...) html_content, + .package = "httr" + ) + + # Test the function with a sample URL + result <- peek_shinylive_app("http://example.com") + + # Verify the result is a quarto_shinylive_apps object + testthat::expect_s3_class(result, "quarto_shinylive_apps") +}) + +test_that("peek_shinylive_app(): handles app.json content correctly", { + # Create sample JSON content representing a standalone Shiny application + json_content <- jsonlite::toJSON(list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + )) + + # Mock HTTP-related functions for JSON response + testthat::local_mocked_bindings( + # Mock GET to return JSON content with appropriate headers + GET = function(...) base::structure( + list( + headers = list("content-type" = "application/json"), + content = base::charToRaw(json_content) + ), + class = "response" + ), + http_error = function(...) FALSE, + content = function(...) json_content, + .package = "httr" + ) + + # Test the function with a URL pointing to app.json + result <- peek_shinylive_app("http://example.com/app.json") + + # Verify the result is a standalone_shinylive_app object + testthat::expect_s3_class(result, "standalone_shinylive_app") +}) + +test_that("peek_quarto_shinylive_app(): handles app-dir format correctly", { + # Create sample HTML content with a Shiny application + html_content <- ' +
+    #| viewerHeight: 500
+    ## file: app.R
+    library(shiny)
+    ui <- fluidPage()
+    server <- function(input, output) {}
+    shinyApp(ui, server)
+    
+ ' + + # Mock HTTP-related functions + testthat::local_mocked_bindings( + GET = function(...) base::structure( + list( + headers = list("content-type" = "text/html"), + content = base::charToRaw(html_content) + ), + class = "response" + ), + http_error = function(...) FALSE, + content = function(...) html_content, + .package = "httr" + ) + + # Create temporary directory for output + temp_dir <- base::tempfile() + + # Test the function with app-dir output format + result <- peek_quarto_shinylive_app( + "http://example.com", + output_format = "app-dir", + output_path = temp_dir + ) + + # Verify result type and format + testthat::expect_s3_class(result, "quarto_shinylive_apps") + testthat::expect_equal(result$output_format, "app-dir") + + # Clean up temporary directory + base::unlink(temp_dir, recursive = TRUE) +}) + +# Test peek_standalone_shinylive_app() ---- + +test_that("peek_standalone_shinylive_app(): processes standalone app correctly", { + # Create sample app.json content + app_json <- list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + + # Mock HTTP-related functions + testthat::local_mocked_bindings( + # Mock GET to return JSON content with appropriate headers + GET = function(...) base::structure( + list( + headers = list("content-type" = "application/json"), + content = base::charToRaw(jsonlite::toJSON(app_json)) + ), + class = "response" + ), + http_error = function(...) FALSE, + content = function(...) jsonlite::toJSON(app_json), + .package = "httr" + ) + + # Create temporary directory for output + temp_dir <- base::tempfile() + + # Test standalone app processing + result <- peek_standalone_shinylive_app( + "http://example.com/app.json", + output_dir = temp_dir + ) + + # Verify result type and file creation + testthat::expect_s3_class(result, "standalone_shinylive_app") + testthat::expect_true(base::file.exists(base::file.path(temp_dir, "app.R"))) + + # Clean up temporary directory + base::unlink(temp_dir, recursive = TRUE) +}) diff --git a/tests/testthat/test-quarto-cell-parser.R b/tests/testthat/test-quarto-cell-parser.R new file mode 100644 index 0000000..75c2397 --- /dev/null +++ b/tests/testthat/test-quarto-cell-parser.R @@ -0,0 +1,83 @@ +# Test parse_code_block() ---- + +test_that("parse_code_block(): handles different types of blocks", { + # Test an R code block with multiple components: + # - YAML-style options + # - Multiple files + # - Different content types + r_code <- '#| viewerHeight: 500 +#| standalone: true +## file: app.R +library(shiny) +ui <- fluidPage() +server <- function(input, output) {} +shinyApp(ui, server) +## file: data.csv +## type: text +x,y +1,2' + + # Parse the R code block + r_result <- parse_code_block(r_code, "r") + + # Verify engine identification + testthat::expect_equal(r_result$engine, "r") + + # Verify YAML options were parsed correctly + testthat::expect_equal(r_result$options$viewerHeight, 500) + testthat::expect_true(r_result$options$standalone) + + # Verify all expected files were identified and named correctly + testthat::expect_true( + base::all(c("app.R", "data.csv") %in% base::names(r_result$files)) + ) + + # Test a Python code block with simpler structure: + # - Single YAML option + # - Single file + # - No explicit type declaration + py_code <- ' + #| viewerHeight: 400 + ## file: app.py + from shiny import App + ' + + # Parse the Python code block + py_result <- parse_code_block(py_code, "python") + + # Verify Python-specific parsing + testthat::expect_equal(py_result$engine, "python") + testthat::expect_equal(py_result$options$viewerHeight, 400) + testthat::expect_true("app.py" %in% base::names(py_result$files)) +}) + +# Test parse_yaml_options() ---- + +test_that("parse_yaml_options handles different value types", { + # Test parsing of different YAML value types: + # - Numeric (viewerHeight) + # - Boolean (standalone) + # - Array (components) + # - String (layout) + yaml_lines <- c( + "#| viewerHeight: 500", # Numeric value + "#| standalone: true", # Boolean value + "#| components: [viewer,editor]", # Array value + "#| layout: vertical" # String value + ) + + # Parse the YAML options + result <- parse_yaml_options(yaml_lines) + + # Verify numeric values are parsed correctly + testthat::expect_equal(result$viewerHeight, 500) + + # Verify array values are split and processed correctly + testthat::expect_equal(result$components, c("viewer", "editor")) + + # Verify boolean values are converted properly + testthat::expect_true(result$standalone) + + # Verify string values remain as strings + testthat::expect_equal(result$layout, "vertical") +}) diff --git a/tests/testthat/test-shinylive-quarto-apps-commands.R b/tests/testthat/test-shinylive-quarto-apps-commands.R new file mode 100644 index 0000000..a4b3b03 --- /dev/null +++ b/tests/testthat/test-shinylive-quarto-apps-commands.R @@ -0,0 +1,107 @@ +# Test create_quarto_shinylive_apps() ---- + +test_that("create_quarto_shinylive_apps(): creates object with correct structure", { + # Create sample apps list with an R Shiny application + apps <- list( + list( + engine = "r", # Specify R as the engine + options = list(viewerHeight = 500), # Set viewer height option + files = list( + app.R = list( + name = "app.R", # Main R app file + content = "library(shiny)\n...", # Sample content + type = "text" # File type + ) + ) + ) + ) + + # Create Quarto Shinylive apps object + result <- create_quarto_shinylive_apps( + apps = apps, # List of apps + output_format = "app-dir", # Output as directory structure + output_path = "test_path" # Base path for output + ) + + # Verify object structure + testthat::expect_s3_class(result, "quarto_shinylive_apps") # Check class + testthat::expect_equal(result$apps, apps) # Check apps list + testthat::expect_equal(result$output_format, "app-dir") # Check format + testthat::expect_equal(result$output_path, "test_path") # Check path +}) + +# Test print.quarto_shinylive_apps() ---- + +test_that("print.quarto_shinylive_apps(): displays correct output for app-dir format", { + # Create temporary directory for test files + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + base::on.exit(base::unlink(temp_dir, recursive = TRUE)) + + # Create sample apps list with both R and Python apps + apps <- list( + # R Shiny app + list( + engine = "r", + options = list(), + files = list(app.R = list(name = "app.R", content = "", type = "text")) + ), + # Python Shiny app + list( + engine = "python", + options = list(), + files = list(app.py = list(name = "app.py", content = "", type = "text")) + ) + ) + + # Create object for testing + obj <- create_quarto_shinylive_apps(apps, "app-dir", temp_dir) + + # Capture all output (both stdout and stderr for CLI messages) + output <- base::c( + utils::capture.output(base::print(obj), type = "output"), + utils::capture.output(base::print(obj), type = "message") + ) + + # Combine output for pattern matching + output_str <- base::paste(output, collapse = "\n") + + # Verify output contains expected sections + testthat::expect_match(output_str, "Shinylive Applications") # Main header + testthat::expect_match(output_str, "Shiny for R Applications") # R section + testthat::expect_match(output_str, "Shiny for Python Applications") # Python section +}) + +test_that("print.quarto_shinylive_apps(): displays correct output for quarto format", { + # Create temporary directory for test files + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + base::on.exit(base::unlink(temp_dir, recursive = TRUE)) + + # Create sample apps list with R app + apps <- list( + list( + engine = "r", + options = list(), + files = list(app.R = list(name = "app.R", content = "", type = "text")) + ) + ) + + # Create object with quarto format + obj <- create_quarto_shinylive_apps( + apps, + "quarto", # Quarto document format + base::file.path(temp_dir, "test.qmd") # Output as .qmd file + ) + + # Capture all output + output <- base::c( + utils::capture.output(base::print(obj), type = "output"), + utils::capture.output(base::print(obj), type = "message") + ) + output_str <- base::paste(output, collapse = "\n") + + # Verify Quarto-specific output + testthat::expect_match(output_str, "Quarto Document with Shinylive Applications") # Main header + testthat::expect_match(output_str, "Setup and Preview Steps") # Setup instructions +}) diff --git a/tests/testthat/test-shinylive-standalone-app-commands.R b/tests/testthat/test-shinylive-standalone-app-commands.R new file mode 100644 index 0000000..a8daae0 --- /dev/null +++ b/tests/testthat/test-shinylive-standalone-app-commands.R @@ -0,0 +1,96 @@ +# Test create_standalone_shinylive_app() ---- + +test_that("create_standalone_shinylive_app(): creates object with correct structure", { + # Create sample app data representing a simple R Shiny application + app_data <- list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + + # Create standalone app object with the sample data + result <- create_standalone_shinylive_app( + app_data = app_data, # List containing file data + output_dir = "test_dir", # Directory where files will be written + url = "https://example.com/app.json" # Source URL of the application + ) + + # Verify the object has the correct S3 class + testthat::expect_s3_class(result, "standalone_shinylive_app") + + # Check if all components match the input data + testthat::expect_equal(result$files, app_data) # Files should match input + testthat::expect_equal(result$output_dir, "test_dir") # Output dir should match + testthat::expect_equal(result$source_url, "https://example.com/app.json") # URL should match +}) + +# Test print.standalone_shinylive_app() ---- + +test_that("print.standalone_shinylive_app(): displays correct output for R app", { + # Create temporary directory for test files + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + + # Ensure cleanup of temporary directory after test + base::on.exit(base::unlink(temp_dir, recursive = TRUE)) + + # Create sample R Shiny app data + app_data <- list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + + # Create standalone app object + obj <- create_standalone_shinylive_app(app_data, temp_dir, "https://example.com") + + # Capture both standard output and messages from print + output <- base::c( + utils::capture.output(base::print(obj), type = "output"), + utils::capture.output(base::print(obj), type = "message") + ) + + # Combine output lines into single string for pattern matching + output_str <- base::paste(output, collapse = "\n") + + # Verify output contains expected headers and app type + testthat::expect_match(output_str, "Standalone Shinylive Application") + testthat::expect_match(output_str, "Type: R Shiny") +}) + +test_that("print.standalone_shinylive_app(): displays correct output for Python app", { + # Create temporary directory for test files + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + + # Ensure cleanup of temporary directory after test + base::on.exit(base::unlink(temp_dir, recursive = TRUE)) + + # Create sample Python Shiny app data + app_data <- list( + list( + name = "app.py", + content = "from shiny import App\n...", + type = "text" + ) + ) + + # Create standalone app object + obj <- create_standalone_shinylive_app(app_data, temp_dir, "https://example.com") + + # Capture both standard output and messages from print + output <- base::c( + utils::capture.output(base::print(obj), type = "output"), + utils::capture.output(base::print(obj), type = "message") + ) + + # Combine output lines into single string for pattern matching + output_str <- base::paste(output, collapse = "\n") + + # Verify output correctly identifies Python app type + testthat::expect_match(output_str, "Type: Python Shiny") +}) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 0000000..cfb5630 --- /dev/null +++ b/tests/testthat/test-utils.R @@ -0,0 +1,59 @@ +# Test validate_app_json() ---- + +test_that("validate_app_json(): checks structure correctly", { + # Test case 1: Valid app.json structure + # Contains all required fields: name, content, and type + valid_data <- list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + # Should return TRUE for valid structure + testthat::expect_true(validate_app_json(valid_data)) + + # Test case 2: Empty list + # app.json must contain at least one file + testthat::expect_error( + validate_app_json(list()), + "File list is empty" + ) + + # Test case 3: Missing required fields + # Each file entry must have name, content, and type + testthat::expect_error( + validate_app_json(list(list(name = "app.R"))), + "Missing required fields" + ) + + # Test case 4: Invalid data type + # app.json must be a list/array + testthat::expect_error( + validate_app_json("not a list"), + "Expected a list" + ) +}) + +# Test padding_width() ---- + +test_that("padding_width(): calculates correct width", { + # Test single digit numbers (1-9) + # Should return padding width of 1 + testthat::expect_equal(padding_width(1), 1) + testthat::expect_equal(padding_width(9), 1) + + # Test two digit numbers (10-99) + # Should return padding width of 2 + testthat::expect_equal(padding_width(10), 2) + testthat::expect_equal(padding_width(99), 2) + + # Test three digit numbers (100+) + # Should return padding width of 3 + testthat::expect_equal(padding_width(100), 3) + + # Test edge cases + # Zero and negative numbers should return minimum width of 1 + testthat::expect_equal(padding_width(0), 1) # Zero case + testthat::expect_equal(padding_width(-1), 1) # Negative number case +}) diff --git a/tests/testthat/test-writers.R b/tests/testthat/test-writers.R new file mode 100644 index 0000000..8a4b266 --- /dev/null +++ b/tests/testthat/test-writers.R @@ -0,0 +1,171 @@ +# Test write_file_content() ---- + +test_that("write_file_content(): handles text content correctly", { + # Create temporary directory for test files with cleanup + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + base::on.exit(base::unlink(temp_dir, recursive = TRUE)) + + # Test nested path creation and UTF-8 character handling + file_path <- base::file.path(temp_dir, "nested", "test.txt") + # Include non-ASCII characters to test UTF-8 handling + content <- "Hello 世界!\nSecond line" + + # Write the content to file with text type + write_file_content(content, file_path, type = "text") + + # Verify nested directory structure was created + testthat::expect_true(base::dir.exists(base::dirname(file_path))) + testthat::expect_true(base::file.exists(file_path)) + + # Verify content was written correctly with proper UTF-8 encoding + testthat::expect_equal( + base::readLines(file_path, warn = FALSE, encoding = "UTF-8"), + base::strsplit(content, "\n")[[1]] + ) +}) + +test_that("write_file_content(): handles binary content correctly", { + # Create temporary directory with cleanup + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + base::on.exit(base::unlink(temp_dir, recursive = TRUE)) + + file_path <- base::file.path(temp_dir, "test.png") + # Create sample base64 encoded PNG content + content <- base::paste0( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA", + "DUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + ) + + # Write binary content + write_file_content(content, file_path, type = "binary") + + # Verify file exists and has correct size + testthat::expect_true(base::file.exists(file_path)) + testthat::expect_equal(base::file.info(file_path)$size, 70L) +}) + +# Test write_apps_to_quarto() ---- + +test_that("write_apps_to_quarto(): creates correct Quarto document", { + # Create temporary Quarto markdown file + temp_file <- base::tempfile(fileext = ".qmd") + + # Create sample app data structure + apps <- list( + list( + engine = "r", + options = list(viewerHeight = 500), + files = list( + "app.R" = list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + ) + ) + + # Write apps to Quarto document + write_apps_to_quarto(apps, temp_file) + + # Read generated document content + content <- base::readLines(temp_file) + + # Verify Quarto document structure + testthat::expect_true(base::any(base::grepl("^---$", content))) # YAML frontmatter + testthat::expect_true(base::any(base::grepl("shinylive", content))) # Shinylive extension + testthat::expect_true(base::any(base::grepl("```\\{shinylive-r\\}", content))) # Code block syntax + testthat::expect_true(base::any(base::grepl("#\\| viewerHeight: 500", content))) # YAML options + + # Cleanup temporary file + base::unlink(temp_file) +}) + +# Test write_apps_to_dirs() ---- + +test_that("write_apps_to_dirs creates correct directory structure", { + # Create temporary directory + temp_dir <- base::tempfile() + base::dir.create(temp_dir) + + # Create sample apps with different engines + apps <- list( + # R Shiny app + list( + engine = "r", + options = list(viewerHeight = 500), + files = list( + "app.R" = list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ) + ) + ), + # Python Shiny app + list( + engine = "python", + options = list(), + files = list( + "app.py" = list( + name = "app.py", + content = "from shiny import App\n...", + type = "text" + ) + ) + ) + ) + + # Write apps to directories + write_apps_to_dirs(apps, temp_dir) + + # Verify directory structure and files + testthat::expect_true(base::dir.exists(base::file.path(temp_dir, "app_1"))) # First app dir + testthat::expect_true(base::dir.exists(base::file.path(temp_dir, "app_2"))) # Second app dir + testthat::expect_true(base::file.exists(base::file.path(temp_dir, "app_1", "app.R"))) # R file + testthat::expect_true(base::file.exists(base::file.path(temp_dir, "app_2", "app.py"))) # Python file + # Check metadata file + testthat::expect_true(base::file.exists(base::file.path(temp_dir, "app_1", "shinylive_metadata.json"))) + + # Cleanup + base::unlink(temp_dir, recursive = TRUE) +}) + +# Test write_standalone_shinylive_app() ---- + +test_that("write_standalone_shinylive_app creates correct file structure", { + # Create temporary directory + temp_dir <- base::tempfile() + + # Create sample app data with nested structure + json_data <- list( + list( + name = "app.R", + content = "library(shiny)\n...", + type = "text" + ), + list( + name = "data/example.csv", # Nested file structure + content = "x,y\n1,2", + type = "text" + ) + ) + + # Write standalone app + result <- write_standalone_shinylive_app( + json_data, + "https://example.com/app.json", + temp_dir + ) + + # Verify file structure and metadata + testthat::expect_true(base::file.exists(base::file.path(temp_dir, "app.R"))) # Main app file + testthat::expect_true(base::file.exists(base::file.path(temp_dir, "data", "example.csv"))) # Nested data file + testthat::expect_s3_class(result, "standalone_shinylive_app") # Correct return type + testthat::expect_equal(result$source_url, "https://example.com/app.json") # Source URL preserved + + # Cleanup + base::unlink(temp_dir, recursive = TRUE) +})