Skip to content

feat(browser): introduce toMatchScreenshot for Visual Regression Testing #8041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 78 commits into
base: main
Choose a base branch
from

Conversation

macarie
Copy link

@macarie macarie commented May 27, 2025

Description

This PR introduces initial support for Visual Regression Testing for Vitest via a new toMatchScreenshot assertion.

Related issue: #6265

In this initial iteration:

  • The feature supports PNG screenshots and uses pixelmatch as the comparator.
  • The architecture is extensible: new comparators or codecs can be added easily, as long as they implement the expected interface.
  • Comparators and codecs can be asynchronous.
  • Screenshot comparison is executed in Node as a browser command, which allows for future adoption of native codecs or comparators.

The logic to get a stable screenshot follows Playwright's approach (with some differences):

  1. Uses as baseline an optional reference screenshot or captures a new screenshot.
  2. Takes a screenshot and compares it to the baseline.
  3. If they match, the page is considered stable and the function returns.
  4. If not, it continues with the latest screenshot as the baseline.
  5. Repeats until stability is reached or a timeout is hit.

The command has 6 possible outcomes:

Outcome Description Result
#01 couldn't get a stable screenshot within timeout fail
#02 stable screenshot taken, but no reference exists and update is not allowed fail
#03 stable screenshot taken, no reference exists, but updates are allowed fail
#04 stable screenshot taken on first try and matches reference pass
#05 stable screenshot taken matches reference after retries pass
#06 stable screenshot taken doesn't match reference (fallback case) fail

The client matcher expects an Element or Locator along with:

  • An optional name for the screenshot
  • An optional options object that includes:
    • Screenshot options, excluding conflicting ones
    • Comparator name and options
    • Timeout value

To-do

  • Fix screenshot paths and naming
  • Add global config for matcher, including resolveScreenshotPath and resolveDiffPath functions
  • Handle default config in one place
  • Use annotation API (feat: annotation API #7953) to show screenshot paths
  • Support thresholds in pixelmatch comparator
  • Add documentation
  • Write test cases

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
  • Ideally, include a test that fails without this PR but passes with it.
  • Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.

Tests

  • Run the tests with pnpm test:ci.

Documentation

  • If you introduce new functionality, document it. You can run documentation with pnpm run docs command.

Changesets

  • Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.

Copy link

netlify bot commented May 27, 2025

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 32c1313
🔍 Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/6876dadf15cba60008880ad6
😎 Deploy Preview https://deploy-preview-8041--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Member

@sheremet-va sheremet-va left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good overall, just some code style issues

@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch 2 times, most recently from 6a8e1ab to d5848f0 Compare May 29, 2025 23:00
@macarie
Copy link
Author

macarie commented May 29, 2025

I moved the types around to what starts to hopefully make some sense. I created a shared folder for the types used in both browser and Node environments.

I've also rebased and resolved the conflicts from main.

@@ -1,6 +1,7 @@
// Disable automatic exports.

import { ARIARole } from './aria-role.ts'
import { ComparatorRegistry, ScreenshotMatcherOptions } from './context.js'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this creates a circular dependency between this file and context.d.ts, was wondering if it's fine tho.

To fix it I would have to move ScreenshotOptions out of context.d.ts and, as it's using Locator, at least half of the other types declared in it 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it works, it works 😄

@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from dbd7baa to 2412183 Compare June 1, 2025 02:03
@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch 2 times, most recently from 79afa30 to addc827 Compare June 2, 2025 18:40
@@ -56,7 +56,7 @@ function parseInspector(inspect: string | undefined | boolean | number) {
return { host, port: Number(port) || defaultInspectPort }
}

export function resolveApiServerConfig<Options extends ApiConfig & UserConfig>(
export function resolveApiServerConfig<Options extends ApiConfig & Omit<UserConfig, 'expect'>>(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is provided with the browser object, the new browser.expect property conflicts with expect. I noticed options.expect is not used tho, so instead of removing it at runtime I removed it from the type.

Comment on lines +241 to +250
toMatchScreenshot?: {
[ComparatorName in keyof ToMatchScreenshotComparators]:
{
/**
* The name of the comparator to use for visual diffing.
*
* @defaultValue `'pixelmatch'`
*/
comparatorName?: ComparatorName
comparatorOptions?: ToMatchScreenshotComparators[ComparatorName]
}
}[keyof ToMatchScreenshotComparators] & ToMatchScreenshotOptions
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is a bit chaotic, but without generics the only solution is to create discriminated unions.

The interface is compatible with ScreenshotComparatorRegistry, so I used that to extend it in the providers. In the future this will allow to add more comparators and have them typed correctly in the config by augmenting ScreenshotComparatorRegistry.

Copy link
Member

@sheremet-va sheremet-va left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me, but we need a lot more tests for this feature

Copy link
Member

@sheremet-va sheremet-va left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me, but we need a lot more tests for this feature

@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from ff2c9ad to 8649b09 Compare June 4, 2025 22:33
@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from 7fab222 to 682bbd1 Compare June 8, 2025 22:17
@macarie
Copy link
Author

macarie commented Jun 8, 2025

I changed the testing approach: instead of relying on pre-generated screenshots, the tests now create them dynamically during execution.

This makes the tests more stable and avoids failures across different OSes, browser versions, or rendering environments. All artifacts are cleaned up automatically after the tests run.

One thing still missing is a test for watch mode. I ran into some issues with it and will take another shot at getting it to work tomorrow. ➡️ Done.

@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from ab114c5 to 246d7c0 Compare June 11, 2025 22:09
Comment on lines +219 to +240
while (signal.aborted === false) {
if (decodedBaseline === null) {
decodedBaseline = takeDecodedScreenshot(screenshotArgument)
}

const [image1, image2] = await Promise.all([
decodedBaseline,
takeDecodedScreenshot(screenshotArgument),
])

const comparatorResult = (await comparator(
image1,
image2,
{ ...comparatorOptions, createDiff: false },
)).pass

decodedBaseline = image2

if (comparatorResult) {
break
}

retries += 1
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Playwright uses a backoff mechanism based on predefined timeouts ([0, 100, 250, 500], fallbacks to 1_000) when retrying.

I'm not sure if I should implement that, if each tests waits 100ms to take a screenshot again, it adds up.

Right now, it's surprisingly hard not to get a stable screenshot in a few retries, but this doesn't mean the timing is consistent across runs. When using Playwright as a provider, consistency should not be an issue because they allow disabling animations. With WebdriverIO I don't know as I have no experience with it.

@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch 2 times, most recently from c611092 to e2776e9 Compare June 15, 2025 21:06
@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from d900731 to c7c6219 Compare June 17, 2025 22:17
@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from 0600986 to 7476ee6 Compare July 14, 2025 22:14
@macarie macarie force-pushed the feat/expect-locator-to-have-screenshot branch from f84095c to 71a616e Compare July 15, 2025 22:47
@macarie
Copy link
Author

macarie commented Jul 15, 2025

Let me know if you don't want the merge commit in the PR's history, I will rebase and remove it once we're done with the review.

Force-pushing after a rebase moves comments up in the "load more" hidden section every time I update from main 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants