From ccd5367de3e7eb9cdbeec0c737e04135d9b12e40 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Wed, 11 Mar 2026 12:36:59 +0530 Subject: [PATCH 1/3] feat: implement multiline text support and fix title positioning --- DESCRIPTION | 1 + NEWS.md | 13 +- R/z_animint.R | 15 +- R/z_animintHelpers.R | 16 +- R/z_multiline.R | 4 + inst/htmljs/animint.js | 137 ++++++++++++------ tests/testthat/test-compiler-multiline-text.R | 92 ++++++++++++ .../test-renderer1-multiline-spacing.R | 21 +++ 8 files changed, 250 insertions(+), 49 deletions(-) create mode 100644 R/z_multiline.R create mode 100644 tests/testthat/test-compiler-multiline-text.R create mode 100644 tests/testthat/test-renderer1-multiline-spacing.R diff --git a/DESCRIPTION b/DESCRIPTION index 43ca2e728..fe548504c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -281,6 +281,7 @@ Collate: 'uu_zzz.r' 'z_animint.R' 'z_animintHelpers.R' + 'z_multiline.R' 'z_facets.R' 'z_geoms.R' 'z_helperFunctions.R' diff --git a/NEWS.md b/NEWS.md index 6184d117b..4a7e1a57f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,11 @@ - `update_axes`: Fixed issue #273 where axis tick text font-size was inconsistent between plots with and without `update_axes`. Previously, plots using `theme_animint(update_axes="x")` would lose `theme(axis.text = element_text(size=...))` styling after axis updates. +# Changes in version 2025.11.7 (PR#261) + +- Fixed multiline text spacing: plot titles no longer overlap with plot area, and X/Y axis label spacing is now consistent. +- Fixed axis titles to scale correctly with `theme(text=element_text(size=X))` (issue #64). + # Changes in version 2025.10.31 (PR#271) - `geom_point()` now warns when shape parameter is set to a value other than 21, since animint2 web rendering only supports shape=21 for proper display of both color and fill aesthetics. @@ -30,6 +35,10 @@ - `geom_text(vjust!=0)` warning mentions vjust support in `geom_label_aligned()`. +# Changes in version 2025.10.21 (PR#221) + +- Multi-line text support: `\n` now works in plot titles, axis titles, legend titles, and `geom_text()` labels. Created `R/z_multiline.R` with helper to convert `\n` to `
` during R compilation. JavaScript renderer converts `
` to SVG `` elements for proper multi-line display. + # Changes in version 2025.10.17 (PR#255) - `getCommonChunk()` uses default group=1 (previously 1:N which was slower). @@ -259,7 +268,7 @@ # Changes in 2022.5.25 -- Add ability to rotate geom_text labels, following ggplot2's semantics of rotation direction. +- Add ability to rotate geom_text labels, following ggplot2\'s semantics of rotation direction. # Changes in 2022.5.24 @@ -324,4 +333,4 @@ # Changes in 2017.08.24 -- DSL: clickSelects/showSelected are now specified as parameters rather than aesthetics. +- DSL: clickSelects/showSelected are now specified as parameters rather than aesthetics. \ No newline at end of file diff --git a/R/z_animint.R b/R/z_animint.R index c50bfee02..8d6b51971 100644 --- a/R/z_animint.R +++ b/R/z_animint.R @@ -130,7 +130,7 @@ parsePlot <- function(meta, plot, plot.name){ for (xy in c("x", "y")) { s <- function(tmp) sprintf(tmp, xy) # one axis name per plot (ie, a xtitle/ytitle is shared across panels) - plot.info[[s("%stitle")]] <- if(is.blank(s("axis.title.%s"))){ + axis_title_raw <- if(is.blank(s("axis.title.%s"))){ "" } else { scale.i <- which(plot$scales$find(xy)) @@ -143,6 +143,10 @@ parsePlot <- function(meta, plot, plot.name){ lab.or.null } } + # Convert newlines to
for multi-line axis titles (Issue #221) + plot.info[[s("%stitle")]] <- convertNewlinesToBreaks(axis_title_raw) + ## axis title size. + plot.info[[s("%stitle_size")]] <- getTextSize(s("axis.title.%s"), theme.pars) ## panel text size. plot.info[[s("strip_text_%ssize")]] <- getTextSize( s("strip.text.%s"), theme.pars) @@ -187,12 +191,13 @@ parsePlot <- function(meta, plot, plot.name){ # grab the unique axis labels (makes rendering simpler) plot.info <- getUniqueAxisLabels(plot.info) - # grab plot title if present - plot.info$title <- if(is(theme.pars$plot.title, "blank")){ + # grab plot title if present and convert newlines (Issue #221) + plot_title_raw <- if(is(theme.pars$plot.title, "blank")){ "" }else{ plot$labels$title } + plot.info$title <- convertNewlinesToBreaks(plot_title_raw) plot.info$title_size <- getTextSize("plot.title", theme.pars) ## Set plot width and height from animint.* options if they are @@ -747,7 +752,6 @@ getLegendList <- function(plistextra){ gdefs <- guides_merge(gdefs) gdefs <- guides_geom(gdefs, layers, default_mapping) } else (zeroGrob()) - names(gdefs) <- sapply(gdefs, function(i) i$title) ## adding the variable used to each LegendList for(leg in seq_along(gdefs)) { @@ -827,6 +831,9 @@ getLegendList <- function(plistextra){ } } legend.list <- lapply(gdefs, getLegend) + # Use the 'class' field from getLegend output for legend key names (Issue #221) + # This ensures JSON keys don't contain newlines or other special characters + names(legend.list) <- sapply(legend.list, function(i) i$class) ## Add a flag to specify whether or not there is both a color and a ## fill legend to display. If so, we need to draw the interior of ## the points in the color legend as the same color. diff --git a/R/z_animintHelpers.R b/R/z_animintHelpers.R index 97cce5a7f..f3b4b2cf1 100644 --- a/R/z_animintHelpers.R +++ b/R/z_animintHelpers.R @@ -702,6 +702,8 @@ getLegend <- function(mb){ names(data) <- paste0(geom, names(data))# aesthetics by geom names(data) <- gsub(paste0(geom, "."), "", names(data), fixed=TRUE) # label isn't geom-specific data$label <- paste(data$label) # otherwise it is AsIs. + # Convert newlines to
for multi-line legend labels (Issue #221) + data$label <- convertNewlinesToBreaks(data$label) data } dataframes <- mapply(function(i, j) cleanData(i$data, mb$key, j, i$params), @@ -726,10 +728,16 @@ getLegend <- function(mb){ if(guidetype=="none"){ NULL }else{ + # Convert newlines to
for multi-line legend title (Issue #221) + legend_title <- convertNewlinesToBreaks(mb$title) + # For the 'class' field (used as JSON key), sanitize newlines to spaces + # to avoid JSON parsing issues with control characters (Issue #221) + safe_title <- gsub("\n", " ", mb$title, fixed = TRUE) + legend_class <- if(mb$is.discrete) mb$selector else safe_title list(guide = guidetype, geoms = unlist(mb$geom.legend.list), - title = mb$title, - class = if(mb$is.discrete)mb$selector else mb$title, + title = legend_title, + class = legend_class, selector = mb$selector, is_discrete= mb$is.discrete, legend_type = mb$legend_type, @@ -913,6 +921,10 @@ split_recursive <- function(x, vars){ ##' @author Toby Dylan Hocking saveChunks <- function(x, meta){ if(is.data.frame(x)){ + # Convert newlines to
in label column for multi-line text (Issue #221) + if("label" %in% names(x)){ + x$label <- convertNewlinesToBreaks(x$label) + } this.i <- meta$chunk.i csv.name <- sprintf("%s_chunk%d.tsv", meta$g$classed, this.i) ## Some geoms should be split into separate groups if there are NAs. diff --git a/R/z_multiline.R b/R/z_multiline.R new file mode 100644 index 000000000..544f4503c --- /dev/null +++ b/R/z_multiline.R @@ -0,0 +1,4 @@ +convertNewlinesToBreaks <- function(text) { + gsub("\n", "
", text, fixed = TRUE) +} + diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index bfcc4b07f..78f5c9441 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -125,24 +125,61 @@ var animint = function (to_select, json_file) { var measureText = function(pText, pFontSize, pAngle, pStyle) { if (pText === undefined || pText === null || pText.length === 0) return {height: 0, width: 0}; if (pAngle === null || isNaN(pAngle)) pAngle = 0; - + + // Create temporary container to measure text var container = element.append('svg'); - // do we need to set the class so that styling is applied? - //.attr('class', classname); - - container.append('text') - .attr({x: -1000, y: -1000}) + var textElement = container.append('text') .attr("transform", "rotate(" + pAngle + ")") .attr("style", pStyle) - .attr("font-size", pFontSize) - .text(pText); - + .attr("font-size", pFontSize); + + // Check if text contains
tags (multi-line) + var textStr = String(pText || ''); + var lines = textStr.split('
'); + + // Always use setMultilineText for consistent rendering + setMultilineText(textElement, pText); + + // Get bounding box after rendering var bbox = container.node().getBBox(); + + // Clean up temporary element container.remove(); - + + // Return measured dimensions return {height: bbox.height, width: bbox.width}; }; +// Set multi-line text on SVG text elements. +// Converts
tags to elements for proper SVG rendering. +var setMultilineText = function(textElement, text) { + textElement.each(function(d) { + var textStr = typeof text === 'function' ? text(d) : text; + // Check for null/undefined, but allow falsy values like 0 or "" + if (textStr === null || textStr === undefined) return; + var lines = String(textStr).split('
'); + var el = d3.select(this); + el.text(''); + // Line height: 1.2em is standard SVG spacing between text lines + var lineHeight = 1.2; + var y = el.attr('y') || 0; + var x = el.attr('x') || 0; + // Get dominant-baseline from parent, if any + var dominantBaseline = el.attr('dominant-baseline'); + lines.forEach(function(line, i) { + var tspan = el.append('tspan') + .attr('x', x) + .attr('dy', i === 0 ? 0 : lineHeight + 'em') + .text(line); + // Inherit dominant-baseline from parent text element if set + if (dominantBaseline) { + tspan.attr('dominant-baseline', dominantBaseline); + } + }); + }); +}; + + var nest_by_group = d3.nest().key(function(d){ return d.group; }); var dirs = json_file.split("/"); dirs.pop(); //if a directory path exists, remove the JSON file from dirs @@ -292,9 +329,10 @@ var animint = function (to_select, json_file) { var npanels = Math.max.apply(null, panel_names); // Note axis names are "shared" across panels (just like the title) - var xtitlepadding = 5 + measureText(p_info["xtitle"], default_axis_px).height; - var ytitlepadding = 5 + measureText(p_info["ytitle"], default_axis_px).height; - + var xtitle_size = p_info["xtitle_size"] || (default_axis_px + "pt"); + var ytitle_size = p_info["ytitle_size"] || (default_axis_px + "pt"); + var xtitlepadding = 5 + measureText(p_info["xtitle"], xtitle_size).height; + var ytitlepadding = 5 + measureText(p_info["ytitle"], ytitle_size).height; // 'margins' are fixed across panels and do not // include title/axis/label padding (since these are not // fixed across panels). They do, however, account for @@ -336,31 +374,42 @@ var animint = function (to_select, json_file) { var titlepadding = measureText(p_info.title, p_info.title_size).height; // why are we giving the title padding if it is undefined? if (p_info.title === undefined) titlepadding = 0; + + // Add extra margin below title for multiline text to prevent overlap + // with plot area. The measureText already accounts for multiline height, + // but we need additional bottom margin. + var titleBottomMargin = 5; // pixels of space below title + plotdim.title.x = p_info.options.width / 2; - plotdim.title.y = titlepadding; - svg.append("text") - .text(p_info.title) + // Position title at top margin, let it extend downward + plotdim.title.y = margin.top; + var titleText = svg.append("text") .attr("class", "plottitle") .attr("font-family", "sans-serif") .attr("font-size", p_info.title_size) .attr("transform", "translate(" + plotdim.title.x + "," + plotdim.title.y + ")") - .style("text-anchor", "middle"); + .style("text-anchor", "middle") + .attr("dominant-baseline", "hanging"); + // Use multi-line text helper for plot titles (Issue #221) + setMultilineText(titleText, p_info.title); // grab max text size over axis labels and facet strip labels + // Base spacing between tick labels and axis titles. + // Y-axis uses 5px base (tick labels extend horizontally). + // X-axis uses 30px base to account for rotated tick labels. var axispaddingy = 5; if(p_info.hasOwnProperty("ylabs") && p_info.ylabs.length){ axispaddingy += Math.max.apply(null, p_info.ylabs.map(function(entry){ - // + 5 to give a little extra space to avoid bad axis labels - // in shiny. + // + 5 to give a little extra space to avoid bad axis labels in shiny. return measureText(entry, p_info.ysize).width + 5; })); } - var axispaddingx = 30; // distance between tick marks and x axis name. + var axispaddingx = 15; // distance between tick marks and x axis name (consistent with axispaddingy=5 base, plus ~10px for text cap-height offset). if(p_info.hasOwnProperty("xlabs") && p_info.xlabs.length){ // TODO: throw warning if text height is large portion of plot height? axispaddingx += Math.max.apply(null, p_info.xlabs.map(function(entry){ - return measureText(entry, p_info.xsize, p_info.xangle).height; + return measureText(entry, p_info.xsize, p_info.xangle).height; })); // TODO: carefully calculating this gets complicated with rotating xlabs //margin.right += 5; @@ -421,7 +470,7 @@ var animint = function (to_select, json_file) { var graph_height = p_info.options.height - nrows * (margin.top + margin.bottom) - strip_height - - titlepadding - n_xaxes * axispaddingx - xtitlepadding; + titlepadding - titleBottomMargin - n_xaxes * axispaddingx - xtitlepadding; // Impose the pixelated aspect ratio of the graph upon the width/height // proportions calculated by the compiler. This has to be done on the @@ -559,7 +608,7 @@ var animint = function (to_select, json_file) { var strip_h = cum_height_per_row[current_row-1]; plotdim.ystart = current_row * plotdim.margin.top + (current_row - 1) * plotdim.margin.bottom + - graph_height_cum + titlepadding + strip_h; + graph_height_cum + titlepadding + titleBottomMargin + strip_h; // room for xaxis title should be distributed evenly across // panels to preserve aspect ratio plotdim.yend = plotdim.ystart + plotdim.graph.height; @@ -761,30 +810,30 @@ var animint = function (to_select, json_file) { } //end of for(layout_i // After drawing all backgrounds, we can draw the axis labels. if(p_info["ytitle"]){ - svg.append("text") - .text(p_info["ytitle"]) + var ytitleText = svg.append("text") .attr("class", "ytitle") .style("text-anchor", "middle") - .style("font-size", default_axis_px + "px") + .style("font-size", ytitle_size) .attr("transform", "translate(" + ytitle_x + "," + (ytitle_top + ytitle_bottom)/2 + - ")rotate(270)") - ; + ")rotate(270)"); + // Use multi-line text helper for y-axis title (Issue #221) + setMultilineText(ytitleText, p_info["ytitle"]); } if(p_info["xtitle"]){ - svg.append("text") - .text(p_info["xtitle"]) + var xtitleText = svg.append("text") .attr("class", "xtitle") .style("text-anchor", "middle") - .style("font-size", default_axis_px + "px") + .style("font-size", xtitle_size) .attr("transform", "translate(" + (xtitle_left + xtitle_right)/2 + "," + xtitle_y + - ")") - ; + ")"); + // Use multi-line text helper for x-axis title (Issue #221) + setMultilineText(xtitleText, p_info["xtitle"]); } Plots[p_name].scales = scales; }; //end of add_plot() @@ -1493,10 +1542,11 @@ var animint = function (to_select, json_file) { .attr("y", toXY("y", "y")) .attr("font-size", get_size) .style("text-anchor", get_text_anchor) - .attr("transform", get_rotate) - .text(function (d) { - return d.label; - }) + .attr("transform", get_rotate); + // Use multi-line text helper for geom_text labels (Issue #221) + setMultilineText(e, function (d) { + return d.label; + }) ; }; eAppend = "text"; @@ -1742,7 +1792,7 @@ var animint = function (to_select, json_file) { "stroke": get_colour_off, "fill": get_fill_off }; - // TODO cleanup. + // TODO cleanup. var select_style_default = ["opacity","stroke","fill"]; g_info.select_style = select_style_default.filter( X => g_info.style_list.includes(X)); @@ -2227,10 +2277,15 @@ var animint = function (to_select, json_file) { var first_th = first_tr.append("th") .attr("align", "left") .attr("colspan", 2) - .text(l_info.title) .attr("class", legend_class) - .style("font-size", l_info.title_size) - ; + .style("font-size", l_info.title_size); + // Use multi-line text helper for legend title (Issue #221) + if (l_info.title && l_info.title.indexOf('
') > -1) { + // Multi-line title: replace
with actual line breaks in HTML + first_th.html(l_info.title.replace(//g, '
')); + } else { + first_th.text(l_info.title); + } var legend_svgs = legend_rows.append("td") .append("svg") .attr("id", function(d){return d["id"]+"_svg";}) diff --git a/tests/testthat/test-compiler-multiline-text.R b/tests/testthat/test-compiler-multiline-text.R new file mode 100644 index 000000000..761ddab07 --- /dev/null +++ b/tests/testthat/test-compiler-multiline-text.R @@ -0,0 +1,92 @@ +context("Multi-line text rendering (Issue #221)") + +test_that("plot title supports multi-line text", { + data <- data.frame(x = 1:5, y = 1:5) + viz <- list( + plot1 = ggplot(data, aes(x, y)) + + geom_point() + + ggtitle("Title Line 1\nTitle Line 2") + ) + info <- animint2dir(viz, "test-title-multiline", open.browser = FALSE) + json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json")) + expect_true(grepl("
", json$plots$plot1$title, fixed = TRUE)) + expect_equal(json$plots$plot1$title, "Title Line 1
Title Line 2") +}) + +test_that("x-axis title supports multi-line text", { + data <- data.frame(x = 1:5, y = 1:5) + viz <- list( + plot1 = ggplot(data, aes(x, y)) + + geom_point() + + xlab("X Axis\nLine 2") + ) + info <- animint2dir(viz, "test-xaxis-multiline", open.browser = FALSE) + json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json")) + expect_true(grepl("
", json$plots$plot1$xtitle, fixed = TRUE)) + expect_equal(json$plots$plot1$xtitle, "X Axis
Line 2") +}) + +test_that("y-axis title supports multi-line text", { + data <- data.frame(x = 1:5, y = 1:5) + viz <- list( + plot1 = ggplot(data, aes(x, y)) + + geom_point() + + ylab("Y Axis\nLine 2") + ) + info <- animint2dir(viz, "test-yaxis-multiline", open.browser = FALSE) + json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json")) + expect_true(grepl("
", json$plots$plot1$ytitle, fixed = TRUE)) + expect_equal(json$plots$plot1$ytitle, "Y Axis
Line 2") +}) + +test_that("geom_text labels support multi-line text", { + data <- data.frame( + x = 1:3, y = 1:3, + label = c("One", "Two\nLines", "Three\nLines\nHere") + ) + viz <- list( + plot1 = ggplot(data, aes(x, y, label = label)) + geom_text() + ) + info <- animint2dir(viz, "test-geomtext-multiline", open.browser = FALSE) + tsv_files <- list.files(info$out.dir, pattern = "text.*\\.tsv$", full.names = TRUE) + expect_true(length(tsv_files) > 0) + text_data <- read.table(tsv_files[1], header = TRUE, sep = "\t", quote = "\"") + multiline_labels <- text_data$label[grepl("
", text_data$label, fixed = TRUE)] + expect_true(length(multiline_labels) >= 2) + expect_true(any(grepl("Two
Lines", multiline_labels, fixed = TRUE))) + expect_true(any(grepl("Three
Lines
Here", multiline_labels, fixed = TRUE))) +}) + +test_that("legend title supports multi-line text", { + data <- data.frame(x = 1:6, y = 1:6, category = rep(c("A", "B", "C"), 2)) + viz <- list( + plot1 = ggplot(data, aes(x, y, color = category)) + + geom_point() + + scale_color_discrete(name = "Category\nName") + ) + info <- animint2dir(viz, "test-legend-multiline", open.browser = FALSE) + json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json")) + expect_true("legend" %in% names(json$plots$plot1)) + legend_keys <- names(json$plots$plot1$legend) + expect_true(length(legend_keys) > 0) + has_multiline_title <- FALSE + for (key in legend_keys) { + legend_title <- json$plots$plot1$legend[[key]]$title + if (!is.null(legend_title) && grepl("
", legend_title, fixed = TRUE)) { + has_multiline_title <- TRUE + expect_equal(legend_title, "Category
Name") + break + } + } + expect_true(has_multiline_title) +}) + + +test_that("convertNewlinesToBreaks works correctly", { + expect_equal(animint2:::convertNewlinesToBreaks("Line1\nLine2"), "Line1
Line2") + expect_equal(animint2:::convertNewlinesToBreaks("A\nB\nC\nD"), "A
B
C
D") + expect_equal(animint2:::convertNewlinesToBreaks("No newlines here"), "No newlines here") + expect_equal(animint2:::convertNewlinesToBreaks(""), "") + result <- animint2:::convertNewlinesToBreaks(c("A\nB", "C", "D\nE\nF")) + expect_equal(result, c("A
B", "C", "D
E
F")) +}) diff --git a/tests/testthat/test-renderer1-multiline-spacing.R b/tests/testthat/test-renderer1-multiline-spacing.R new file mode 100644 index 000000000..4d5f09e72 --- /dev/null +++ b/tests/testthat/test-renderer1-multiline-spacing.R @@ -0,0 +1,21 @@ +acontext("multiline text spacing") +data <- data.frame(x = 1:10, y = 1:10) +viz <- list( + plot1 = ggplot(data, aes(x, y)) + + geom_point() + + ggtitle("Multiline Title\nLine Two\nLine Three") + + ylab("Y Axis\nLabel Two") + + theme(text = element_text(size = 20)) +) +info <- animint2HTML(viz) +test_that("multiline plot title with large font does not overlap plot area", { + title_bbox <- get_element_bbox("text.plottitle") + plot_rect <- get_element_bbox("#plot_plot1 rect.background_rect") + expect_lt(title_bbox$top + title_bbox$height, plot_rect$top) +}) +test_that("multiline y-axis title with large font does not overlap plot area", { + ytitle_bbox <- get_element_bbox("text.ytitle") + plot_rect <- get_element_bbox("#plot_plot1 rect.background_rect") + expect_lt(ytitle_bbox$left + ytitle_bbox$width, plot_rect$left) +}) + From 982378339e053bafba31801ad265956b357e71e2 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Thu, 12 Mar 2026 09:05:31 +0530 Subject: [PATCH 2/3] fix: restore axispaddingx=30 to prevent x-axis title overlap with rotated tick labels --- inst/htmljs/animint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index 78f5c9441..4c80394ea 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -405,7 +405,7 @@ var setMultilineText = function(textElement, text) { return measureText(entry, p_info.ysize).width + 5; })); } - var axispaddingx = 15; // distance between tick marks and x axis name (consistent with axispaddingy=5 base, plus ~10px for text cap-height offset). + var axispaddingx = 30; // distance between tick marks and x axis name. if(p_info.hasOwnProperty("xlabs") && p_info.xlabs.length){ // TODO: throw warning if text height is large portion of plot height? axispaddingx += Math.max.apply(null, p_info.xlabs.map(function(entry){ From 5e456da42d0b87dc73f01b83bb8024b13059ec52 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Thu, 12 Mar 2026 12:49:35 +0530 Subject: [PATCH 3/3] Skip ghpages org-repo tests on PR CI; run JS_coverage under xvfb --- .github/workflows/tests.yaml | 7 ++++++- tests/testthat/test-compiler-ghpages.R | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f5ca6bb34..832f201bf 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -20,6 +20,7 @@ jobs: GITHUB_PAT: ${{ secrets.PAT_GITHUB }} GH_ACTION: "ENABLED" CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + ANIMINT2_TEST_GHPAGES: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') }} steps: - uses: actions/checkout@v3 - name: install and update texlive @@ -48,6 +49,10 @@ jobs: run: | npm install v8-to-istanbul echo "Node modules installed" + + - name: Install xvfb (JS_coverage) + if: matrix.test-suite == 'JS_coverage' + run: /usr/bin/sudo DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb - name: run tests run: | @@ -55,7 +60,7 @@ jobs: bash build.sh elif [ "$TEST_SUITE" == "JS_coverage" ]; then echo "Running testthat with JS coverage collection..." - Rscript -e "source('tests/testthat.R', chdir = TRUE)" + xvfb-run -a Rscript -e "source('tests/testthat.R', chdir = TRUE)" else echo "Running testthat with R coverage collection..." Rscript -e 'covr::codecov(quiet = FALSE, type = "tests", test_files = "tests/testthat.R", flags = "r")' diff --git a/tests/testthat/test-compiler-ghpages.R b/tests/testthat/test-compiler-ghpages.R index 202643c24..4dd1e32f7 100644 --- a/tests/testthat/test-compiler-ghpages.R +++ b/tests/testthat/test-compiler-ghpages.R @@ -29,12 +29,23 @@ expect_Capture <- function(L){ expect_no_Capture <- function(L){ expect_false(file.exists(file.path(L$local_clone,"Capture.PNG"))) } +skip_if_no_org_pat <- function(){ + if(!identical(Sys.getenv("ANIMINT2_TEST_GHPAGES"), "true")){ + skip("Skipping GitHub Pages integration tests (set ANIMINT2_TEST_GHPAGES=true to enable).") + } + token <- Sys.getenv("GITHUB_PAT") + token2 <- Sys.getenv("GITHUB_PAT_GITHUB_COM") + if(identical(token, "") && identical(token2, "")){ + skip("Skipping GitHub Pages integration tests (no GitHub token available).") + } +} ## The test below requires a github token with repo delete ## permission. Read ## https://github.com/animint/animint2/wiki/Testing#installation to ## see how to set that up on your local computer, or on github ## actions. reset_test_repo <- function(){ + skip_if_no_org_pat() ## https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#delete-a-repository says The fine-grained token must have the following permission set: "Administration" repository permissions (write) gh api --method DELETE -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/OWNER/REPO tryCatch({ gh::gh("DELETE /repos/animint-test/animint2pages_test_repo") @@ -124,6 +135,9 @@ test_that("animint2pages raises an error if no GitHub token is present", { ## be called to set the env vars/token. repo.root <- system("git rev-parse --show-toplevel", intern=TRUE) config.file <- file.path(repo.root, ".git", "config") + if(file.access(config.file, 2) != 0){ + skip(sprintf("Skip: can not write %s", config.file)) + } config.old <- file.path(repo.root, ".git", "config.old") file.copy(config.file, config.old, overwrite = TRUE) cat("[credential]\n\tusername = FOO", file=config.file, append=TRUE)