Skip to content

feat(browser): add pauseSvgAnimations stabilization plugin - #385

Open
gregberge wants to merge 1 commit into
mainfrom
claude/pause-svg-animations
Open

gregberge wants to merge 1 commit into
mainfrom
claude/pause-svg-animations

Conversation

@gregberge

Copy link
Copy Markdown
Member

Adds a pauseSvgAnimations stabilization plugin, on by default like pauseGifs. For each screenshot, it holds SVG animations that repeat forever, such as a spinning SVG loader, on their first frame.

SVG SMIL animations (<animate>, <animateTransform>, <animateMotion>, <set>) run on their <svg>'s own clock. Neither CSS rules nor Playwright's animations: "disabled" stop them, so today a spinner is captured on whichever frame its loop has reached. This showed up in Argos's own Storybook: the stories with a pending button were the only screenshots whose bytes changed between runs, and Argos puts ui-button--default at a flakiness of 0.18 over 90 days (argos-ci/argos#2621).

Changes

  • The plugin. Before each screenshot it finds the animations with repeatCount="indefinite" or repeatDur="indefinite", pauses the <svg> elements above them and seeks them to 0. Cleanup seeks each clock back to where it was and unpauses the ones it paused. Opt out with stabilize: { pauseSvgAnimations: false }.
  • Why every <svg> above the animation. Chromium runs an animation on the clock of its nearest <svg>, where the spec and Firefox use the outermost one's. In Chromium 151, pausing only the outermost <svg> left an animation inside a nested one running.
  • Why only endless animations. A finite animation settles by itself, and a seek to 0 would capture a finished one at its start. In Chromium, a fill="freeze" animation that had ended went back to its first value on the seek, whether it had run at load, from beginElement() or from a click. Restoring the clock brought all three back.
  • Tests. A fixture with an endless loop, the same loop inside a nested <svg>, and a finite animation. Three e2e tests check that the loops are held on their first frame and handed back running, that the finite animation is left alone, and that the option turns the plugin off.

Verification

  • Checks. check-types, lint, check-format and the unit tests pass for @argos-ci/browser and @argos-ci/playwright.
  • E2e. All 26 Playwright e2e tests pass, and the three new ones passed 60 repeated runs.
  • Guard. With the plugin unregistered, the "held on their first frame" test fails.
  • On a real suite. I swapped this build of @argos-ci/browser (6.4.5 plus this commit) into the Argos repository. Then I ran its Storybook suite without the workaround that chore(storybook): hold the Loader spinner on its first frame argos#2621 adds, comparing the md5 of every screenshot:
Runs ui-button--default ui-dialog--open-pending
Without the plugin 12 6 different images 4 different images
With the plugin 10 1 image the same frame, but 3 runs are off by one colour level in 87 pixels

The other 88 screenshots were byte-identical throughout. The Dialog's leftover sits inside the spinner's box: one colour level, for example (196, 186, 233) against (195, 186, 232). It only appears when the spinner had moved before the seek, so it comes from the repaint the seek causes. Argos's own comparison ignores it, since its colour-sensitive pass uses odiff with a threshold of 0.0225. A byte comparison does not.

Review notes

  • Behaviour change. Screenshots of pages with an endless SMIL animation change once, to its first frame.
  • Not covered. The plugin misses loops built from chained begins (begin="0s;a.end") rather than repeatCount. It also can't reach SVGs rendered through <img> or CSS, or shadow DOM, since it queries document like the other plugins.
  • Docs. This needs a companion page in argos-ci/docs, like pause-gifs.md, and a line in each SDK reference's stabilize list. That PR isn't open yet.

🤖 Generated with Claude Code

SVG SMIL animations (`<animate>`, `<animateTransform>`, `<animateMotion>`,
`<set>`) run on their `<svg>`'s own clock. Neither CSS rules nor Playwright's
`animations: "disabled"` stop them, so an SVG spinner is captured on whichever
frame its loop has reached, and the screenshot changes from run to run.

The new plugin, on by default like `pauseGifs`, pauses every `<svg>` that holds
an animation repeating forever (`repeatCount` or `repeatDur` set to
`indefinite`) and seeks it to 0 before each screenshot. Cleanup seeks each clock
back and unpauses it. It pauses the whole chain of `<svg>` ancestors, because
Chromium runs an animation on its nearest `<svg>`'s clock, where the spec and
Firefox use the outermost one's.

Animations that end are left alone. They settle by themselves, and a seek to 0
would capture a finished one at its start: in Chromium, a `fill="freeze"`
animation that had ended came back to its first value, whether it had run at
load, from `beginElement()` or from a click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
argos-js-sdk-reference Ready Ready Preview Sep 23, 2026 12:56pm UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 9bd79b01 Deployed Sep 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant