library(testthat) library(ggplot2) test_that("collect_legends errors without patchwork installed", { skip_if(rlang::is_installed("patchwork")) expect_error( collect_legends(list()), "patchwork" ) }) test_that("collect_legends errors if x is not a patchwork object", { skip_if_not_installed("patchwork") expect_error( collect_legends(ggplot()), "`x` must be a patchwork object." ) }) test_that("collect_legends errors if x is a list", { skip_if_not_installed("patchwork") expect_error( collect_legends(list(a = 1)), "`x` must be a patchwork object." ) }) test_that("collect_legends works with valid patchwork object", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 | p2 result <- collect_legends(combined) expect_s3_class(result, "patchwork") }) test_that("collect_legends accepts position parameter", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 | p2 result_bottom <- collect_legends(combined, position = "bottom") expect_s3_class(result_bottom, "patchwork") result_left <- collect_legends(combined, position = "left") expect_s3_class(result_left, "patchwork") }) test_that("collect_legends rejects invalid position", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 | p2 expect_error( collect_legends(combined, position = "invalid"), "'arg' should be one of" ) }) test_that("collect_legends with span = TRUE returns gtable", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 / p2 result <- collect_legends(combined, span = TRUE) expect_s3_class(result, "gtable") }) test_that("collect_legends with numeric span returns gtable", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() combined <- p1 / p2 / p3 # Span single row result <- collect_legends(combined, span = 1) expect_s3_class(result, "gtable") # Span multiple rows result <- collect_legends(combined, span = 1:2) expect_s3_class(result, "gtable") }) test_that("collect_legends errors on invalid span indices", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 / p2 expect_error( collect_legends(combined, span = 5), "span indices must be between" ) }) # ============================================================================= # get_legend() tests # ============================================================================= test_that("get_legend returns a gtable for plot with legend", { p <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() + labs(color = "Cylinders") result <- get_legend(p) expect_s3_class(result, "gtable") }) test_that("get_legend returns NULL or empty grob for plot without legend", { p <- ggplot(mtcars, aes(mpg, wt)) + geom_point() result <- get_legend(p) # May return NULL or zeroGrob depending on ggplot2 version if (!is.null(result)) { expect_true(inherits(result, "zeroGrob") || length(result$grobs) == 0) } }) test_that("get_legend returns NULL for plot with legend.position = 'none'", { p <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() + theme(legend.position = "none") result <- get_legend(p) # Should return NULL or empty gtable if (!is.null(result)) { expect_true(length(result$grobs) == 0 || inherits(result, "zeroGrob")) } }) test_that("get_legend errors if not a ggplot", { expect_error(get_legend("not a plot"), "must be a ggplot object") expect_error(get_legend(list()), "must be a ggplot object") }) test_that("get_legend works with fill aesthetic", { p <- ggplot(mtcars, aes(factor(cyl), fill = factor(cyl))) + geom_bar() + labs(fill = "Cylinders") result <- get_legend(p) expect_s3_class(result, "gtable") }) test_that("get_legend works with legend on different positions", { p_base <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() for (pos in c("right", "left", "top", "bottom")) { p <- p_base + theme(legend.position = pos) result <- get_legend(p) expect_s3_class(result, "gtable") } }) # ============================================================================= # shared_legend() tests # ============================================================================= test_that("shared_legend returns a gtable", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() + labs(title = "Plot 1") p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() + labs(title = "Plot 2") result <- shared_legend(p1, p2, ncol = 2) expect_s3_class(result, "gtable") }) test_that("shared_legend errors with no plots", { expect_error(shared_legend(), "At least one plot must be provided") }) test_that("shared_legend errors if not all ggplots", { p1 <- ggplot(mtcars, aes(mpg, wt)) + geom_point() expect_error( shared_legend(p1, "not a plot"), "All arguments must be ggplot objects" ) }) test_that("shared_legend accepts all positions", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() for (pos in c("right", "left", "top", "bottom")) { result <- shared_legend(p1, p2, ncol = 2, position = pos) expect_s3_class(result, "gtable") } }) test_that("shared_legend accepts ncol parameter", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() result <- shared_legend(p1, p2, p3, ncol = 3) expect_s3_class(result, "gtable") }) test_that("shared_legend accepts nrow parameter", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() result <- shared_legend(p1, p2, p3, nrow = 3) expect_s3_class(result, "gtable") }) test_that("shared_legend accepts both ncol and nrow", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() p4 <- ggplot(mtcars, aes(wt, hp, color = factor(cyl))) + geom_point() result <- shared_legend(p1, p2, p3, p4, ncol = 2, nrow = 2) expect_s3_class(result, "gtable") }) test_that("shared_legend computes dimensions when only ncol given", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() result <- shared_legend(p1, p2, p3, ncol = 2) expect_s3_class(result, "gtable") }) test_that("shared_legend computes dimensions when only nrow given", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() result <- shared_legend(p1, p2, p3, nrow = 2) expect_s3_class(result, "gtable") }) test_that("shared_legend accepts legend_from as integer", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(gear))) + geom_point() # Get legend from second plot result <- shared_legend(p1, p2, ncol = 2, legend_from = 2) expect_s3_class(result, "gtable") }) test_that("shared_legend accepts legend_from as ggplot object", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p_legend <- ggplot(mtcars, aes(mpg, wt, color = factor(gear))) + geom_point() result <- shared_legend(p1, p2, ncol = 2, legend_from = p_legend) expect_s3_class(result, "gtable") }) test_that("shared_legend errors on invalid legend_from index", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() expect_error( shared_legend(p1, p2, ncol = 2, legend_from = 5), "must be between 1 and" ) }) test_that("shared_legend accepts rel_legend_size parameter", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() result <- shared_legend(p1, p2, ncol = 2, rel_legend_size = 0.3) expect_s3_class(result, "gtable") }) test_that("shared_legend handles plots without legend", { p1 <- ggplot(mtcars, aes(mpg, wt)) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp)) + geom_point() # Should either warn or return gtable without legend result <- suppressWarnings(shared_legend(p1, p2, ncol = 2)) expect_s3_class(result, "gtable") }) test_that("shared_legend sets horizontal direction for bottom/top positions", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() # These should work without error (horizontal legend direction is set internally) result_bottom <- shared_legend(p1, p2, ncol = 2, position = "bottom") expect_s3_class(result_bottom, "gtable") result_top <- shared_legend(p1, p2, ncol = 2, position = "top") expect_s3_class(result_top, "gtable") }) # ============================================================================= # collect_legends() span tests for top/bottom positions # ============================================================================= test_that("collect_legends with span works for bottom position", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 | p2 result <- collect_legends(combined, position = "bottom", span = TRUE) expect_s3_class(result, "gtable") }) test_that("collect_legends with span works for top position", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 | p2 result <- collect_legends(combined, position = "top", span = TRUE) expect_s3_class(result, "gtable") }) test_that("collect_legends with numeric span works for bottom position", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() combined <- p1 | p2 | p3 # Span single column result <- collect_legends(combined, position = "bottom", span = 1) expect_s3_class(result, "gtable") # Span multiple columns result <- collect_legends(combined, position = "bottom", span = 1:2) expect_s3_class(result, "gtable") }) test_that("collect_legends with numeric span works for top position", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() combined <- p1 | p2 | p3 result <- collect_legends(combined, position = "top", span = 1:2) expect_s3_class(result, "gtable") }) test_that("collect_legends with span errors on invalid column indices", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 | p2 expect_error( collect_legends(combined, position = "bottom", span = 5), "span indices must be between" ) }) test_that("collect_legends warns when no legend found", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt)) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp)) + geom_point() combined <- p1 | p2 expect_warning( collect_legends(combined, span = TRUE), "No legend found" ) }) test_that("collect_legends with span = TRUE works for left position", { skip_if_not_installed("patchwork") library(patchwork) p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() combined <- p1 / p2 result <- collect_legends(combined, position = "left", span = TRUE) expect_s3_class(result, "gtable") }) # ============================================================================= # shared_legend edge cases # ============================================================================= test_that("shared_legend with neither ncol nor nrow defaults to single row", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) + geom_point() p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) + geom_point() # When neither ncol nor nrow specified, should default to n columns, 1 row result <- shared_legend(p1, p2, p3) expect_s3_class(result, "gtable") }) test_that("shared_legend handles source plot without legend", { p1 <- ggplot(mtcars, aes(mpg, wt)) + geom_point() p2 <- ggplot(mtcars, aes(mpg, hp)) + geom_point() # May or may not warn depending on get_legend behavior result <- suppressWarnings(shared_legend(p1, p2, ncol = 2)) expect_s3_class(result, "gtable") }) test_that("shared_legend with single plot works", { p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) + geom_point() result <- shared_legend(p1) expect_s3_class(result, "gtable") })