Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ gulpfile.js
notes.md
package.json
package-lock.json
.travis.yml
^\.github$
.eslintignore
.eslintrc
.keys
^\.claude$
249 changes: 249 additions & 0 deletions .github/workflows/js-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
name: JS tests

# The procedure for accepting new visual baselines is documented in README.md,
# under "Updating visual test baselines". The comments below explain why the
# workflow is wired the way it is, not how to use it.

on:
push:
workflow_dispatch:
inputs:
test_filter:
description: 'Run only tests matching this name pattern (jest -t). Leave blank for all.'
type: string
default: ''
update_snapshots:
description: 'Regenerate visual baselines and upload them as an artifact for you to commit'
type: boolean
default: true

# One in-flight run per branch; a new push supersedes the previous run rather
# than stacking another full visual job behind it.
#
# Regeneration dispatches get their OWN group, so a routine push cannot cancel
# one. That matters because the baseline upload step is gated on !cancelled(),
# which is false once the concurrency manager cancels a run -- an interrupted
# regeneration would therefore discard the entire regenerated set silently,
# showing only "cancelled" in the Actions UI. On a push event the inputs
# context is empty, so the suffix evaluates to '' and pushes still supersede
# each other as intended.
concurrency:
group: js-tests-${{ github.ref }}${{ inputs.update_snapshots && '-regen' || '' }}
cancel-in-progress: true

jobs:
unit:
name: Lint and compile
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
# This job never launches a browser, so skip the Chromium download.
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
id: install
run: npm ci

# Every test step below runs even if an earlier one failed, so a single
# failure does not hide the rest. The job still reports red. Gated on the
# install succeeding, so a broken npm ci does not cascade.
- name: Lint
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: npx gulp lint

# No `gulp testSpecs` step. This project has no unit specs: rhtmlBuildUtils
# points that task at specTestingDirectory ('theSrc/scripts'), where
# nothing matches '**/*.jest.test.js', and jest exits 1 on "No tests
# found". gulpfile.js therefore stubs the task out and excludes the real
# one. The four suites in theSrc/test/bin are browser tests needing a
# served page, so they are NOT unit specs -- they are already run by the
# visual job below, which passes theSrc/test/bin to jest as a second root
# (snapshotTesting.interactionTestDirectory) after compileInternal and
# connect have built and served the pages. Add this step back when real
# unit specs exist under theSrc/scripts.
#
# Deliberately NOT `gulp build`. That sequence starts with `clean`, which
# deletes ['browser', 'inst', 'man', 'R', '.tmp']. `man/moonplot.Rd`,
# `R/rhtmlMoonPlot.R` and `inst/htmlwidgets/*` are all tracked, and `man`
# is only regenerated by `makeDocs`, which shells out to
# `r --no-save <<< "library(devtools); document()"` -- unavailable here, and
# its failure is swallowed by an unconditional done(null). So `gulp build`
# would silently delete tracked files. These two tasks are the compile
# check we actually want, and neither cleans.
- name: Compile widget bundle
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: npx gulp core compileWidgetEntryPoint

visual:
name: Visual regression tests
runs-on: ubuntu-24.04
timeout-minutes: 45
# Read-only: this job neither pushes nor dispatches. Regenerated baselines
# are uploaded as an artifact for a human to commit -- see the last step.
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

# Fonts are pinned explicitly so text metrics do not drift with the base
# image; label placement is what this widget is for, so a font change
# invalidates baselines.
# The libs are Chrome for Testing's runtime dependencies, which the base
# image does not ship in full. Note the t64 suffixes -- Ubuntu 24.04
# renamed these packages during the 64-bit time_t transition, and the old
# names do not resolve.
- name: Install fonts and Chrome runtime libraries
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
fonts-liberation fonts-dejavu-core fonts-noto-color-emoji \
libnss3 libatk1.0-0t64 libatk-bridge2.0-0t64 libcups2t64 \
libatspi2.0-0t64 libasound2t64 libdrm2 libgbm1 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libpango-1.0-0 libcairo2
sudo fc-cache -f

# puppeteer 19+ downloads browsers into ~/.cache/puppeteer via
# @puppeteer/browsers, outside node_modules, so unlike the old
# .local-chromium location it survives `npm ci` and can be cached. Keyed
# on package-lock.json because that is what pins the puppeteer version,
# and the version determines the browser build that gets downloaded.
- name: Cache Chrome for Testing
uses: actions/cache@v4
with:
path: ~/.cache/puppeteer
key: puppeteer-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
id: install
run: npm ci

# No --env flag: rhtmlBuildUtils constrains --env to ['local', 'travis'],
# so 'ci' comes from build/config/widget.config.js instead. --branch is
# likewise omitted, defaulting to master, so every branch compares
# against master's baselines.
# TEST_FILTER goes through env: rather than being interpolated into the
# run script, so the input is not substituted into the shell command here.
# NB this is mitigation at the YAML layer only -- rhtmlBuildUtils splices
# the -t value unescaped into a second command string that it runs via
# shelljs (/bin/sh -c), so a value with shell metacharacters would still
# be interpreted there. Acceptable: workflow_dispatch already requires
# write access, and anyone with that could edit this file directly.
#
# --acceptNewSnapshots=false stops jest WRITING baselines it would then
# throw away. rhtmlBuildUtils defaults that option to true, which appends
# --ci=0 to the jest command; jest then computes
# updateSnapshot = ci && !updateSnapshot ? 'none' : ... : 'new'
# (jest-config/build/normalize.js), and 'new' means jest-image-snapshot
# writes the baseline into the runner's filesystem and passes. Since the
# baseline upload step only runs on update_snapshots, that file would be
# discarded with the runner. Omitting --ci=0 lets jest infer ci=true from
# CI=true (jest-cli defaults ci to is-ci), giving updateSnapshot='none'.
#
# NB that alone does NOT make a missing baseline fail the build, which is
# why the guard step below exists. jest-image-snapshot returns
# {pass: false} for a missing baseline (src/index.js:225) by returning
# EARLY, before the block that increments snapshotState.unmatched, and
# rhtmlBuildUtils' testSnapshots wraps the expect() in a try/catch that
# swallows the error. So a missing baseline leaves the test green, the
# snapshot counters untouched, and jest exiting 0. A pixel MISMATCH does
# increment unmatched, so that alone does fail the run.
- name: Visual regression tests
if: ${{ !cancelled() && steps.install.outcome == 'success' && !inputs.update_snapshots }}
env:
TEST_FILTER: ${{ inputs.test_filter }}
run: |
if [ -n "$TEST_FILTER" ]; then
npx gulp testVisual --acceptNewSnapshots=false -t "$TEST_FILTER"
else
npx gulp testVisual --acceptNewSnapshots=false
fi

# The catch block in rhtmlBuildUtils' testSnapshots writes an image into
# new_snapshots/ for every snapshot that either mismatched or had no
# baseline, so a non-empty new_snapshots/ is a reliable failure signal
# regardless of jest's exit code -- see the NB on the step above. Without
# this, the very first CI run (before theSrc/test/snapshots/ci exists)
# would report success having compared nothing at all.
# Skipped on a regeneration dispatch, where -u writes baselines through
# the success path and never populates new_snapshots/.
- name: Fail if any snapshot was missing or mismatched
if: ${{ !cancelled() && steps.install.outcome == 'success' && !inputs.update_snapshots }}
run: |
new_snapshots=$(find theSrc/test/snapshots/ci -path '*/new_snapshots/*' -name '*.png' 2>/dev/null)
if [ -n "$new_snapshots" ]; then
echo "::error::Snapshots without a committed baseline, or differing from it:"
echo "$new_snapshots"
echo
echo "Download the snapshot-diffs artifact to inspect them. If the new"
echo "rendering is correct, accept it by re-running this workflow with"
echo "update_snapshots ticked -- see README.md."
exit 1
fi
echo "Every snapshot matched a committed baseline."

# Same no---env and env:-passthrough reasoning as the step above. -u makes
# jest-image-snapshot write baselines instead of failing on mismatch.
- name: Regenerate baselines
if: ${{ !cancelled() && steps.install.outcome == 'success' && inputs.update_snapshots }}
env:
TEST_FILTER: ${{ inputs.test_filter }}
run: |
if [ -n "$TEST_FILTER" ]; then
npx gulp testVisual -u -t "$TEST_FILTER"
else
npx gulp testVisual -u
fi

- name: Upload snapshot diffs
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: snapshot-diffs
path: |
theSrc/test/snapshots/ci/**/__diff_output__/**
theSrc/test/snapshots/ci/**/new_snapshots/**
if-no-files-found: ignore
retention-days: 14

# Regenerated baselines are uploaded for a human to commit, NOT committed
# by CI. Two GitHub behaviours make a bot-authored head commit unusable:
#
# 1. A push made with the default GITHUB_TOKEN does not trigger any
# workflow (anti-recursion), so build-r-package.yaml -- which triggers
# only on push -- never runs on that commit.
# 2. workflow_dispatch check runs are excluded from a pull request's
# status rollup. They exist on the commit and go green, but the PR
# reports "no checks reported" and branch protection cannot see them.
#
# Net effect of committing from CI was a PR that looked untested. Uploading
# instead means the human's own push produces the full check set.
#
# Runs even if regeneration exited non-zero: a partial regeneration is
# still worth inspecting alongside the diffs.
- name: Upload regenerated baselines
if: ${{ !cancelled() && inputs.update_snapshots }}
uses: actions/upload-artifact@v4
with:
name: regenerated-baselines
# Exclude the diagnostic directories; they are uploaded separately as
# snapshot-diffs and are gitignored, so they must not be mistaken for
# baselines when this artifact is extracted over a working tree.
path: |
theSrc/test/snapshots/ci
!theSrc/test/snapshots/ci/**/__diff_output__
!theSrc/test/snapshots/ci/**/new_snapshots
if-no-files-found: error
retention-days: 14
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ node_modules
browser
.tmp
theSrc/internal_www/scratch.html
__diff_output__
__diff_output__
new_snapshots
21 changes: 0 additions & 21 deletions .travis.yml

This file was deleted.

5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Author: Displayr <opensource@displayr.com>
Maintainer: Displayr <opensource@displayr.com>
Description: An opinionated template for the creation of html widget repositories using ES6
Imports:
htmlwidgets
htmlwidgets,
jsonlite
License: GPL-3
LazyData: TRUE
RoxygenNote: 7.1.1
RoxygenNote: 7.3.3
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

export(moonplot)
import(htmlwidgets)
import(jsonlite)
3 changes: 3 additions & 0 deletions R/rhtmlMoonPlot.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#'
#' @param coreNodes : Coordinates of nodes in the center of the moon (assumes coreNodes is transformed output of MASS::corresp then $rscore[, 1:2])
#' @param surfaceNodes : Coordinates of nodes outside of the moon (assumes surfaceNodes is transformed output of MASS::corresp then $cscore[, 1:2])
#' @param width : Ignored but must be passed
#' @param height : Ignored but must be passed
#' @param core.font.family : Font family for core labels
#' @param core.font.size : Font size for core labels
#' @param core.font.color : Font color for core labels
Expand Down Expand Up @@ -39,6 +41,7 @@
#' @param link.width : The width of the label links
#'
#' @import htmlwidgets
#' @import jsonlite
#'
#' @export
moonplot <- function(
Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![](https://travis-ci.org/Displayr/rhtmlMoonPlot.svg?branch=master)](https://travis-ci.org/Displayr/rhtmlMoonPlot/)
[![JS tests](https://github.com/Displayr/rhtmlMoonPlot/actions/workflows/js-tests.yaml/badge.svg?branch=master)](https://github.com/Displayr/rhtmlMoonPlot/actions/workflows/js-tests.yaml)
[![Coverage Status](https://coveralls.io/repos/github/Displayr/rhtmlMoonPlot/badge.svg?branch=master)](https://coveralls.io/github/Displayr/rhtmlMoonPlot?branch=master)
# rhtmlMoonPlot

Expand All @@ -24,6 +24,51 @@ version of R in order to build packages from source. Rtools can be downloaded fr
Specifying `dependencies = NA` in `install_github` will not install packages listed
in `Suggests` in the `DESCRIPTION` file (some of which may be proprietary and unavailable for download).

## Updating visual test baselines

The `JS tests` workflow runs automatically on every push. Its `Visual regression tests` job compares
rendered output against the committed baselines in `theSrc/test/snapshots/ci/master` (CI always
compares against `master`'s baselines, whatever branch it is running on). Any intended change to
rendering, layout or label placement will turn the job red and the baselines have to be regenerated.
A missing baseline also fails, rather than being silently accepted.

Baselines are environment specific — locally generated snapshots (`npm run localTest`, which writes
to `theSrc/test/snapshots/local/<branch>`) will not match CI's fonts and Chromium build, so do not
copy them into `theSrc/test/snapshots/ci`. Regenerate through CI instead:

1. **Inspect the failure first.** Download the `snapshot-diffs` artifact from the failed run and check
the `__diff_output__` images. Only regenerate once you are satisfied every diff is intended.
2. **Dispatch a regeneration run.** Actions → `JS tests` → *Run workflow*, select your branch, and
tick `update_snapshots`. Optionally set `test_filter` (passed to `jest -t`) to regenerate only the
tests matching a name pattern; leave it blank to regenerate all of them.
3. **Download the `regenerated-baselines` artifact** from that run. Its contents are rooted at
`master/`, so extract it into `theSrc/test/snapshots/ci/` — not over the repository root.
4. **Review, commit and push the changed snapshots yourself.** Use `git status` / `git diff --stat` to
confirm only the snapshots you expected have changed.

Steps 2 and 3 can be done from the command line with the [GitHub CLI](https://cli.github.com/)
instead of the Actions UI:

```sh
# Dispatch a regeneration run on the current branch (add -f test_filter=<pattern> to narrow it)
gh workflow run "JS tests" --ref "$(git rev-parse --abbrev-ref HEAD)" -f update_snapshots=true

# Get the run id, then follow it to completion
gh run list --workflow "JS tests" --event workflow_dispatch --limit 1
gh run watch <run-id>

# Extract the baselines straight into place -- the artifact is rooted at master/
gh run download <run-id> -n regenerated-baselines -D theSrc/test/snapshots/ci

# And the diffs from a failed comparison run, if you want them on disk
gh run download <run-id> -n snapshot-diffs -D .tmp/diffs
```

CI deliberately does not commit the baselines for you. A push made with the default `GITHUB_TOKEN`
does not trigger any workflow, and `workflow_dispatch` check runs are excluded from a pull request's
status rollup — so a bot-authored head commit would leave the PR reporting no checks. Pushing the
snapshots yourself produces the full set of checks on the PR.

## Submitting a bug report

If you encounter a problem using the package, please open an [issue](https://github.com/Displayr/rhtmlMoonPlot/issues). To achieve a resolution as quickly as possible, please include a minimal, reproducible example of the bug, along with the exact error message or output you receive and the behavior you expect. Including the output of `sessionInfo()` in R can be helpful to reproduce the issue. Please see this [FAQ](https://community.rstudio.com/t/faq-whats-a-reproducible-example-reprex-and-how-do-i-create-one/5219), which has a number of useful tips on creating great reproducible examples.
Expand Down
Loading
Loading