Skip to content

fix: restore pseudo states and correct iframe sizing in Vitest SDKs - #356

Open
gregberge wants to merge 2 commits into
mainfrom
fix/vitest-storybook-screenshot-regressions
Open

fix: restore pseudo states and correct iframe sizing in Vitest SDKs#356
gregberge wants to merge 2 commits into
mainfrom
fix/vitest-storybook-screenshot-regressions

Conversation

@gregberge

Copy link
Copy Markdown
Member

Two fixes for regressions in the Vitest-based SDKs, surfaced after the dependency upgrade in #354.

1. Pseudo states lost in portable stories (@argos-ci/storybook)

This is what build 39470 flagged. Out of 30 diffs, exactly 2 changed: example-button--primary-hover in both modes. The baseline is a red (hovered) button, the head is a plain blue one.

storybook-addon-pseudo-states rewrites :hover rules into .pseudo-hover ones only when it receives storyRendered on the Storybook channel, and it grabs that channel when it is imported. Portable stories run without the preview runtime, so no channel is installed and addons.getChannel() falls back to a mock one:

  • Storybook 10.4 created that mock once and cached it (this.setChannel(channel)), so the addon's listener and the storyRendered Argos emits landed on the same object.
  • Storybook 10.5 returns a throwaway mock per call unless a channel has been installed, so the event went nowhere.

Instrumenting at screenshot time showed it precisely: the root element still had its pseudo-hover-all class, but every CSS rule had __processed: null and getChannel().listeners("storyRendered").length === 0.

The fix installs a real Channel from a setup file registered ahead of the user's, so one exists before any addon preview module captures it. The existing setup file stays last on purpose — afterEach hooks run in reverse registration order, and that is what makes the screenshot happen before Storybook unmounts the story.

2. Screenshots cut off, and iframe size leaking between tests

Both @argos-ci/vitest and the @argos-ci/storybook Vitest plugin grow the Vitest tester iframe to fit the content, because anything overflowing the iframe box is never painted. Two bugs there:

The iframe was measured before the content had settled. The fit ran from beforeScreenshot, which the Playwright SDK calls before waitForReadiness. An <img> that has not loaded takes no space, so the iframe was sized from a partially-loaded layout and everything below that height came out blank. Now stabilization runs first, so images and fonts are loaded before the content is measured.

The grown iframe was never restored. The "initial" branch of setIframeViewportSize was guarded on the backed-up width being truthy, but the original inline width is an empty string — and the default (non-viewports) path never asked for a restore at all. Vitest reuses one iframe per file, so each screenshot inherited the largest previous one: a single <button> was being captured at 2000x1600 in our own e2e suite. The backup now lives under a dedicated attribute whose presence marks the size as saved, and it is restored once the screenshot is taken.

Tests

  • packages/vitest/e2e: a Vite middleware serves an image after a delay, and the test decodes the resulting PNG to assert the bottom band is not blank. Confirmed to fail without the stabilization fix.
  • packages/vitest/e2e: a small capture taken after the tall/wide ones asserts it did not inherit their size. Confirmed to fail without the restore fix.
  • packages/vitest/src/command.test.ts: pins that stabilization runs before the iframe is measured, and that the size is restored.
  • packages/storybook/src/vitest-plugin.test.ts (new unit project): pins both setup-file positions. Confirmed to fail if the unshift becomes a push.

Snapshot sizes in the Vitest e2e suite are now content-correct (aria.png 2000x1600 → 414x896), example-button--primary-hover renders red again and differs from example-button--primary in both modes, and the Storybook Vitest e2e produces identical dimensions to before for every other story.

Note

Fix 2 is verified against a deterministic local reproduction, but it is not what build 39470 showed — that build was entirely fix 1. It was written for the second, user-reported "screenshots are cut again" case, which I could not open. It applies if that project runs Storybook + Vitest (or @argos-ci/vitest); if it uses the Playwright-based Storybook test-runner, no iframe fit is involved and this will not address it. Worth confirming before we call that report closed.

🤖 Generated with Claude Code

gregberge and others added 2 commits August 7, 2026 15:38
Screenshots taken through `@argos-ci/vitest` and the `@argos-ci/storybook`
Vitest plugin were cut off, with the bottom of the page left blank.

Both SDKs grow the Vitest tester iframe to fit the content, because anything
overflowing the iframe box is never painted. They did that from
`beforeScreenshot`, which the Playwright SDK runs *before* it waits for images
and fonts. An `<img>` that has not loaded takes no space, so the iframe was
sized from a partially-loaded layout and everything below that height was never
painted. Run the stabilization first, so the content has reached its final size
before it is measured.

The iframe was also never restored afterwards. The `"initial"` branch of
`setIframeViewportSize` was guarded on the backed-up width being truthy, but the
original inline width is an empty string, and the default (non-`viewports`) path
never asked for a restore at all. Vitest reuses the same iframe for every test
in a file, so each screenshot inherited the largest previous one — a single
`<button>` was captured at 2000x1600 in our own e2e suite. Track the backup
under a dedicated attribute, whose presence marks the size as saved, and restore
it once the screenshot is taken.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Since the Storybook 10.5 upgrade, stories relying on
`storybook-addon-pseudo-states` lost their pseudo state: `Primary Hover`
rendered as a plain blue button instead of the red hovered one.

The addon only rewrites `:hover` rules into `.pseudo-hover` ones when it
receives `storyRendered` on the Storybook channel, and it grabs that channel
when it is imported. Portable stories (Vitest) run without the preview runtime,
so no channel is installed and `addons.getChannel()` falls back to a mock one.
Up to 10.4 that mock was created once and cached, so the addon's listener and
the `storyRendered` Argos emits landed on the same object. Since 10.5 a
throwaway mock is returned per call, so the event went nowhere: the root element
still got its `pseudo-hover-all` class, but no rule had been rewritten to match
it.

Install a real channel in a setup file registered ahead of the user's, so it
exists before any addon preview module captures one. The existing setup file
stays last: `afterEach` hooks run in reverse registration order, and that is
what makes the screenshot happen before Storybook unmounts the story.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
argos-js-sdk-reference Ready Ready Preview Aug 7, 2026 2:30pm

Request Review

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