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 c7173ba..ef27586 100644 --- a/R/testthat-helper-plots.R +++ b/R/testthat-helper-plots.R @@ -3,6 +3,10 @@ #' 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. #' +#' 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). #' For \pkg{ggplot2} objects, a structural fallback snapshot is also maintained. #' When a visual comparison passes, missing structural snapshots are created in #' interactive test runs, in update mode (\code{options(jaspTools.plotStructure.update = TRUE)} @@ -17,7 +21,7 @@ #' \code{options(jaspTools.plotSnapshot.gc = FALSE)} to disable this. #' #' -#' @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')` #' @@ -42,6 +46,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) @@ -55,12 +62,25 @@ 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) freshVisual <- is_fresh_visual_snapshot(result) @@ -78,7 +98,7 @@ expect_plot_with_fallback <- function(name, test, tolerance = NULL) { if (freshVisual) fail_fresh_visual_snapshot(name, result) - 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(build_structural_fallback_review_message(name, result), 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 2356de1..4cb8da8 100644 --- a/man/expect_equal_plots.Rd +++ b/man/expect_equal_plots.Rd @@ -7,7 +7,7 @@ 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).} @@ -22,6 +22,13 @@ This function compares a stored .svg of a plot, to the plot that is created when 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. For \pkg{ggplot2} objects, a structural fallback snapshot is also maintained. When a visual comparison passes, missing structural snapshots are created in interactive test runs, in update mode (\code{options(jaspTools.plotStructure.update = TRUE)} diff --git a/tests/testthat/test-expect-equal-plots-fallback.R b/tests/testthat/test-expect-equal-plots-fallback.R index 194b7f9..4c7219f 100644 --- a/tests/testthat/test-expect-equal-plots-fallback.R +++ b/tests/testthat/test-expect-equal-plots-fallback.R @@ -290,3 +290,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) +})