diff --git a/DESCRIPTION b/DESCRIPTION index c47d586..d9d907a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -29,5 +29,5 @@ Imports: vdiffr (>= 1.0.7) Suggests: ggplot2 -RoxygenNote: 7.3.3 Roxygen: list(markdown = TRUE) +Config/roxygen2/version: 8.0.0 diff --git a/R/testthat-helper-plots.R b/R/testthat-helper-plots.R index 2e86e01..e3c92c9 100644 --- a/R/testthat-helper-plots.R +++ b/R/testthat-helper-plots.R @@ -3,7 +3,8 @@ #' This function compares a stored .svg of a plot, to the plot that is created when the tests are run. #' If no visual reference (.svg) exists yet, \pkg{vdiffr} handles it like other visual snapshots. #' -#' For \pkg{ggplot2} objects, a structural fallback snapshot is also maintained. +#' Plot recipes are materialized before they are passed to \pkg{vdiffr}. For +#' rendered \pkg{ggplot2} objects, a structural fallback snapshot is also maintained. #' In interactive test runs, if that structural snapshot is missing, it is created automatically #' (even when the visual comparison passes). #' @@ -11,7 +12,7 @@ #' package root after running tests. #' #' -#' @param test The plot object you wish to test (does not work well for non-ggplot2 objects). +#' @param test The plot object or plot recipe you wish to test. #' @param name The name of the reference plot (a .svg stored in /tests/testthat/_snaps). #' @param dir `r lifecycle::badge('deprecated')` #' @@ -36,6 +37,9 @@ expect_equal_plots <- function(test, name, dir = lifecycle::deprecated(), tolera return() } + originalTest <- test + test <- materialize_plot_for_vdiffr(test) + skip_if_grob(test) skip_if_recordedPlot(test) @@ -49,22 +53,35 @@ expect_equal_plots <- function(test, name, dir = lifecycle::deprecated(), tolera if (inherits(test, "qgraph")) { qq <- test test <- function() plot(qq) + originalTest <- test } - expect_plot_with_fallback(name, test, tolerance = tolerance) + fallbackTest <- if (is_ggplot(test)) test else originalTest + expect_plot_with_fallback(name, test, fallback_test = fallbackTest, tolerance = tolerance) } } -expect_plot_with_fallback <- function(name, test, tolerance = NULL) { +materialize_plot_for_vdiffr <- function(test) { + if (!("jaspPlotRecipe" %in% sub("^.*::", "", class(test)))) + return(test) + + if (!requireNamespace("jaspGraphs", quietly = TRUE)) + stop("Package 'jaspGraphs' is required to render a jaspPlotRecipe.", call. = FALSE) + + materialize <- getExportedValue("jaspGraphs", "materializeJaspPlotRecipe") + materialize(test) +} + +expect_plot_with_fallback <- function(name, test, fallback_test = test, tolerance = NULL) { result <- capture_vdiffr_expectation(name, test) if (isTRUE(result$passed)) { - maybe_seed_ggplot_structure_snapshot(test, name) + maybe_seed_ggplot_structure_snapshot(fallback_test, name) return(invisible(TRUE)) } # Save the CI-generated SVG so it can be uploaded as an artifact for comparison save_failed_plot_svg(test, name) - fallbackResult <- expect_doppelganger_fallback(test, name, vdiffr_result = result, tolerance = tolerance) + fallbackResult <- expect_doppelganger_fallback(fallback_test, name, vdiffr_result = result, tolerance = tolerance) if (isTRUE(fallbackResult$passed)) { warning("vdiffr mismatch for '", name, "' accepted by structural fallback.", call. = FALSE) testthat::succeed(paste0("vdiffr mismatch for '", name, "' accepted by fallback.")) diff --git a/man/expect_equal_plots.Rd b/man/expect_equal_plots.Rd index eac8d4e..bfb30f6 100644 --- a/man/expect_equal_plots.Rd +++ b/man/expect_equal_plots.Rd @@ -7,17 +7,28 @@ expect_equal_plots(test, name, dir = lifecycle::deprecated(), tolerance = NULL) } \arguments{ -\item{test}{The plot object you wish to test (does not work well for non-ggplot2 objects).} +\item{test}{The plot object or plot recipe you wish to test.} \item{name}{The name of the reference plot (a .svg stored in /tests/testthat/_snaps).} \item{dir}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}} -\item{tolerance}{Optional numeric tolerance for the structural fallback comparison (default \code{1e-6}). Set to a larger value (e.g. \code{1e-3}) for plots whose data vary across platforms (e.g. MCMC-based Bayesian plots).} +\item{tolerance}{Optional numeric tolerance for the structural fallback +comparison (default \code{1e-6}). Set to a larger value (e.g. \code{1e-3}) +for plots whose data vary across platforms (e.g. MCMC-based Bayesian plots).} } \description{ This function compares a stored .svg of a plot, to the plot that is created when the tests are run. -If no .svg exists yet then you must first run \code{manageTestPlots}. +If no visual reference (.svg) exists yet, \pkg{vdiffr} handles it like other visual snapshots. +} +\details{ +Plot recipes are materialized before they are passed to \pkg{vdiffr}. For +rendered \pkg{ggplot2} objects, a structural fallback snapshot is also maintained. +In interactive test runs, if that structural snapshot is missing, it is created automatically +(even when the visual comparison passes). + +To accept changed structural snapshots, use \code{testthat::snapshot_accept()} from the +package root after running tests. } \examples{ diff --git a/tests/testthat/test-expect-equal-plots-fallback.R b/tests/testthat/test-expect-equal-plots-fallback.R index eed0a75..8cb9aaa 100644 --- a/tests/testthat/test-expect-equal-plots-fallback.R +++ b/tests/testthat/test-expect-equal-plots-fallback.R @@ -121,3 +121,71 @@ test_that("compare_ggplot_structure_snapshot returns TRUE only for equal structu expect_true(jaspTools:::compare_ggplot_structure_snapshot(f1, f2)) expect_false(jaspTools:::compare_ggplot_structure_snapshot(f1, f3)) }) + +test_that("vdiffr and fallback can use different plot objects", { + rendered <- structure(list(), class = "rendered-plot") + original <- structure(list(), class = "original-plot") + seen <- new.env(parent = emptyenv()) + + testthat::local_mocked_bindings( + capture_vdiffr_expectation = function(name, test) { + seen$vdiffr <- test + list(passed = FALSE, exception = simpleError("forced mismatch")) + }, + save_failed_plot_svg = function(...) invisible(NULL), + expect_doppelganger_fallback = function(test, ...) { + seen$fallback <- test + list(passed = TRUE, has_fallback = TRUE, exception = NULL, message = NULL) + }, + .package = "jaspTools" + ) + + expect_warning( + jaspTools:::expect_plot_with_fallback("separate-targets", rendered, fallback_test = original), + "accepted by structural fallback" + ) + expect_identical(seen$vdiffr, rendered) + expect_identical(seen$fallback, original) +}) + +test_that("materialized ggplot is used for vdiffr and structural fallback", { + skip_if_not_installed("ggplot2") + + recipe <- structure(list(fun = "example"), class = "jaspPlotRecipe") + rendered <- ggplot2::ggplot(mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + seen <- new.env(parent = emptyenv()) + + testthat::local_mocked_bindings( + materialize_plot_for_vdiffr = function(test) rendered, + expect_plot_with_fallback = function(name, test, fallback_test, ...) { + seen$vdiffr <- test + seen$fallback <- fallback_test + invisible(TRUE) + }, + .package = "jaspTools" + ) + + jaspTools::expect_equal_plots(recipe, "recipe-plot") + expect_identical(seen$vdiffr, rendered) + expect_identical(seen$fallback, rendered) +}) + +test_that("non-ggplot recipe rendering keeps the recipe as fallback", { + recipe <- structure(list(fun = "example"), class = "jaspPlotRecipe") + rendered <- function() graphics::plot(1:3) + seen <- new.env(parent = emptyenv()) + + testthat::local_mocked_bindings( + materialize_plot_for_vdiffr = function(test) rendered, + expect_plot_with_fallback = function(name, test, fallback_test, ...) { + seen$vdiffr <- test + seen$fallback <- fallback_test + invisible(TRUE) + }, + .package = "jaspTools" + ) + + jaspTools::expect_equal_plots(recipe, "recipe-function-plot") + expect_identical(seen$vdiffr, rendered) + expect_identical(seen$fallback, recipe) +})