# ============================================================================= # test-plot.R — Unit tests for plot_forest() and plot_tableone() # Focus: input validation + pure internal helpers (no graphics device needed) # ============================================================================= # =========================================================================== # Shared helpers # =========================================================================== .fake_forest_df <- function() { data.frame( item = c("AD vs control", "Crude", "Adjusted"), cases_n = c("", "89/4521", "89/4521"), stringsAsFactors = FALSE ) } .fake_t1_df <- function(n = 120, seed = 42) { set.seed(seed) data.frame( age = round(rnorm(n, 57, 8), 1), sex = factor(sample(c("Male", "Female"), n, TRUE)), bmi = round(rnorm(n, 27, 5), 1), trt = factor(sample(c("Drug A", "Drug B"), n, TRUE)), stringsAsFactors = FALSE ) } # =========================================================================== # plot_forest() — input validation # =========================================================================== test_that("plot_forest() aborts on non-data.frame input", { expect_error(plot_forest("not a df", 1, 0.5, 2), "data.frame") }) test_that("plot_forest() aborts when est length != nrow(data)", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(1, 2), lower = c(0.5, 1), upper = c(2, 3)), "length 3" ) }) test_that("plot_forest() aborts when lower length != nrow(data)", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(0.5, 1.18), # wrong length upper = c(NA, 1.96, 1.85)), "length 3" ) }) test_that("plot_forest() aborts when upper length != nrow(data)", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(1.96, 1.85)), # wrong length "length 3" ) }) test_that("plot_forest() aborts when indent length != nrow(data)", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), indent = c(0L, 1L)), # length 2, need 3 "length" ) }) test_that("plot_forest() aborts when bold_label length != nrow(data)", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), bold_label = c(TRUE, FALSE)), # length 2, need 3 "length" ) }) test_that("plot_forest() aborts when ci_col is wrong vector length", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), ci_col = c("black", "red")), # length 2, need 1 or 3 "length" ) }) test_that("plot_forest() aborts when p_cols not in data", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), p_cols = "nonexistent"), "nonexistent" ) }) test_that("plot_forest() aborts when save=TRUE and dest=NULL", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), save = TRUE, dest = NULL), "dest" ) }) test_that("plot_forest() aborts when bold_p is not logical", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), bold_p = "yes"), "logical" ) }) test_that("plot_forest() aborts when ci_column < 2", { df <- .fake_forest_df() expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), ci_column = 1L), "ci_column" ) }) test_that("plot_forest() aborts when ci_column > ncol(data) + 1", { df <- .fake_forest_df() # 2 cols expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), ci_column = 4L), # max allowed = 3 "ci_column" ) }) test_that("plot_forest() aborts when header length != ncol(data) + 2", { df <- .fake_forest_df() # 2 orig cols → nc_final = 4 expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), header = c("Label", "Gap")), # need 4 "length" ) }) test_that("plot_forest() aborts when align length != ncol(data) + 2", { df <- .fake_forest_df() # nc_final = 4 expect_error( plot_forest(df, est = c(NA, 1.52, 1.43), lower = c(NA, 1.18, 1.11), upper = c(NA, 1.96, 1.85), align = c(-1L, 0L)), # need 4 "length" ) }) # =========================================================================== # .fp_build_data() — pure data processing helper # =========================================================================== test_that(".fp_build_data() inserts gap_ci and OR (95% CI) columns", { df <- data.frame(item = c("A", "B"), stringsAsFactors = FALSE) out <- ukbflow:::.fp_build_data( df, c(1.2, NA), c(0.9, NA), c(1.6, NA), indent = c(0L, 0L), p_cols = NULL, p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 2L ) expect_true("gap_ci" %in% names(out$data)) expect_true("OR (95% CI)" %in% names(out$data)) }) test_that(".fp_build_data() formats non-NA est as 'est (lower, upper)'", { df <- data.frame(item = c("A", "B"), stringsAsFactors = FALSE) out <- ukbflow:::.fp_build_data( df, c(1.52, NA), c(1.18, NA), c(1.96, NA), indent = c(0L, 0L), p_cols = NULL, p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 2L ) ci_col <- out$data[["OR (95% CI)"]] expect_equal(ci_col[1L], "1.52 (1.18, 1.96)") expect_equal(ci_col[2L], "") }) test_that(".fp_build_data() applies indent as non-breaking spaces", { df <- data.frame(item = c("Parent", "Child"), stringsAsFactors = FALSE) out <- ukbflow:::.fp_build_data( df, c(NA, 1.2), c(NA, 0.9), c(NA, 1.6), indent = c(0L, 1L), p_cols = NULL, p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 2L ) expect_equal(out$data[[1L]][1L], "Parent") expect_true(startsWith(out$data[[1L]][2L], "\u00a0\u00a0")) }) test_that(".fp_build_data() formats p_cols numeric to string and caches originals", { df <- data.frame(item = c("A", "B"), p = c(0.031, NA_real_), stringsAsFactors = FALSE) out <- ukbflow:::.fp_build_data( df, c(1, NA), c(0.5, NA), c(2, NA), indent = c(0L, 0L), p_cols = "p", p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 2L ) expect_equal(out$data$p[1L], "0.031") expect_equal(out$data$p[2L], "") expect_equal(out$p_numeric$p[1L], 0.031) }) test_that(".fp_build_data() clips p < 0.001 to '<0.001'", { df <- data.frame(item = "A", p = 0.00001, stringsAsFactors = FALSE) out <- ukbflow:::.fp_build_data( df, 1.2, 0.9, 1.6, indent = 0L, p_cols = "p", p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 2L ) expect_equal(out$data$p[1L], "<0.001") }) test_that(".fp_build_data() aborts when ci_column < 2", { df <- data.frame(item = "A", stringsAsFactors = FALSE) expect_error( ukbflow:::.fp_build_data( df, 1, 0.5, 2, indent = 0L, p_cols = NULL, p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 1L ), ">= 2" ) }) test_that(".fp_build_data() maps p_col indices correctly when p_col is right of ci_column", { # data: item | n | p → ci_column = 2 → gap_ci inserted at 2 # original p is col 3, after insert becomes col 5 df <- data.frame(item = "A", n = "10/100", p = 0.05, stringsAsFactors = FALSE) out <- ukbflow:::.fp_build_data( df, 1, 0.5, 2, indent = 0L, p_cols = "p", p_digits = 3L, ci_digits = 2L, ci_sep = ", ", ci_column = 2L ) # orig p is col 3, >= ci_column (2) → idx = 3 + 2 = 5 expect_equal(out$p_col_idxs, 5L) }) # =========================================================================== # .fp_theme() — theme builder # =========================================================================== test_that(".fp_theme() returns a list for 'default'", { th <- ukbflow:::.fp_theme("default") expect_true(is.list(th)) }) test_that(".fp_theme() result has expected ci element", { th <- ukbflow:::.fp_theme("default") # forest_theme stores CI settings under $ci expect_true(!is.null(th$ci) || !is.null(th$ci_col)) }) test_that(".fp_theme() warns on unknown preset and falls back to default", { expect_warning( ukbflow:::.fp_theme("unknown_theme"), "Unknown theme" ) }) test_that(".fp_theme() different ci_Theight values do not error", { expect_no_error(ukbflow:::.fp_theme("default", ci_Theight = 0.1)) expect_no_error(ukbflow:::.fp_theme("default", ci_Theight = 0.5)) }) # =========================================================================== # plot_tableone() — input validation # =========================================================================== test_that("plot_tableone() aborts on non-data.frame input", { expect_error(plot_tableone("not a df", vars = "age"), "data.frame") }) test_that("plot_tableone() aborts when vars not in data", { df <- .fake_t1_df() expect_error( plot_tableone(df, vars = "nonexistent", save = FALSE), "missing" ) }) test_that("plot_tableone() aborts when strata not in data", { df <- .fake_t1_df() expect_error( plot_tableone(df, vars = "age", strata = "missing_col", save = FALSE), "missing_col" ) }) test_that("plot_tableone() aborts when save=TRUE and dest=NULL", { df <- .fake_t1_df() expect_error( plot_tableone(df, vars = "age", strata = "trt", save = TRUE, dest = NULL), "dest" ) }) test_that("plot_tableone() warns and disables add_p when strata=NULL", { skip_on_cran() df <- .fake_t1_df() # cli::cli_warn() produces a warning, not a message expect_warning( plot_tableone(df, vars = "age", strata = NULL, add_p = TRUE, save = FALSE), "add_p" ) }) test_that("plot_tableone() warns and disables add_smd when strata=NULL", { skip_on_cran() df <- .fake_t1_df() # cli::cli_warn() produces a warning, not a message. # Disable add_p to avoid a second unrelated warning leaking out. expect_warning( plot_tableone(df, vars = "age", strata = NULL, add_p = FALSE, add_smd = TRUE, save = FALSE), "add_smd" ) }) test_that("plot_tableone() aborts when exclude_labels is not character", { df <- .fake_t1_df() expect_error( plot_tableone(df, vars = "age", strata = "trt", exclude_labels = 1L, save = FALSE), "character" ) }) # =========================================================================== # .t1_smd_continuous() — Cohen's d helper # =========================================================================== test_that(".t1_smd_continuous() returns non-negative numeric for two groups", { set.seed(1) x <- c(rnorm(50, mean = 0), rnorm(50, mean = 1)) grp <- factor(rep(c("A", "B"), each = 50)) val <- ukbflow:::.t1_smd_continuous(x, grp, levels(grp)) expect_true(is.numeric(val)) expect_gte(val, 0) }) test_that(".t1_smd_continuous() returns ~0 when groups have same mean", { x <- c(rep(5, 50), rep(5, 50)) grp <- factor(rep(c("A", "B"), each = 50)) val <- ukbflow:::.t1_smd_continuous(x, grp, levels(grp)) # pooled SD = 0 → NA (division by zero guard), or 0 if identical expect_true(is.na(val) || val == 0) }) test_that(".t1_smd_continuous() returns NA when a group has < 2 observations", { x <- c(1, 2, 3) grp <- factor(c("A", "A", "B")) # group B has only 1 obs val <- ukbflow:::.t1_smd_continuous(x, grp, levels(grp)) expect_true(is.na(val)) }) # =========================================================================== # .t1_smd_categorical() — RMSD helper # =========================================================================== test_that(".t1_smd_categorical() returns non-negative numeric", { x <- factor(c("Yes", "No", "Yes", "Yes", "No", "No")) grp <- factor(c("A", "A", "A", "B", "B", "B")) val <- ukbflow:::.t1_smd_categorical(x, grp, levels(grp)) expect_true(is.numeric(val)) expect_gte(val, 0) }) test_that(".t1_smd_categorical() returns NA for single-category variable", { x <- factor(c("Yes", "Yes", "Yes", "Yes")) grp <- factor(c("A", "A", "B", "B")) val <- ukbflow:::.t1_smd_categorical(x, grp, levels(grp)) expect_true(is.na(val)) }) test_that(".t1_smd_categorical() returns ~0 when proportions are identical", { x <- factor(c("Yes", "No", "Yes", "No")) grp <- factor(c("A", "A", "B", "B")) val <- ukbflow:::.t1_smd_categorical(x, grp, levels(grp)) expect_lt(val, 1e-10) }) # =========================================================================== # .t1_compute_smd() — per-variable SMD dispatcher # =========================================================================== test_that(".t1_compute_smd() returns one row per variable", { set.seed(42) df <- .fake_t1_df() res <- ukbflow:::.t1_compute_smd(df, vars = c("age", "sex"), strata = "trt") expect_equal(nrow(res), 2L) expect_equal(res$variable, c("age", "sex")) }) test_that(".t1_compute_smd() smd_fmt is character or NA", { df <- .fake_t1_df() res <- ukbflow:::.t1_compute_smd(df, vars = c("age", "bmi", "sex"), strata = "trt") expect_true(is.character(res$smd_fmt)) }) test_that(".t1_compute_smd() smd_fmt is formatted to 3 decimal places", { df <- .fake_t1_df() res <- ukbflow:::.t1_compute_smd(df, vars = "age", strata = "trt") fmt <- res$smd_fmt[!is.na(res$smd_fmt)] if (length(fmt) > 0L) { # e.g. "0.123" — decimal part is exactly 3 digits expect_true(all(grepl("^\\d+\\.\\d{3}$", fmt))) } })