Skip to content

Add visual regression assertions - #174

Open
smnandre wants to merge 5 commits into
playwright-php:mainfrom
smnandre:feat/visual-regression
Open

smnandre wants to merge 5 commits into
playwright-php:mainfrom
smnandre:feat/visual-regression

Conversation

@smnandre

Copy link
Copy Markdown
Member

Add toHaveScreenshot() for pages and locators, using Playwright's comparator and screenshot stabilization.

  • Support PNG and WebP baselines, update modes, masks and configurable tolerances.
  • Save expected, actual and diff images separately for each baseline.
  • Reject non-finite numeric options before calling the browser.
  • Include documentation and regression tests.

Notes:

  • Negated assertions are not supported yet.
  • The (internal) implementation uses Playwright's private _expectScreenshot() API.

The bridge gains an expectScreenshot command for pages and locators. It
calls page._expectScreenshot(), the private method behind
@playwright/test's toHaveScreenshot(): screenshots until two consecutive
ones match, masks, then pixelmatch or ssim-cie94. The bridge applies the
defaults the JS runner adds on its side (animations disabled, caret
hidden, CSS scale). Images travel base64 encoded, masks as selectors
rebuilt into locators, frames included.

Page::expectScreenshot() and Locator::expectScreenshot() expose it as
@internal methods returning a ScreenshotComparison. PageInterface and
LocatorInterface are left untouched: downstream test doubles implement
them.
toHaveScreenshot() on page and locator assertions, and on expect() in
test cases, compares a stable screenshot with a baseline image, as in
@playwright/test:

- Baselines live next to the test file, <Test>.php-snapshots/, named
  after the browser of the page under test and the platform:
  card-chromium-linux.png. PNG by default, lossless WebP with .webp.
- PLAYWRIGHT_UPDATE_SNAPSHOTS takes the --update-snapshots values: none,
  missing (default), changed, all. "all" records without comparing.
- A failure writes card-expected, card-actual and card-diff to
  test-failures/<TestClass>-<testMethod>-<browser>/, removed once the
  baseline matches or is recorded again.
- ToHaveScreenshotOptions carries the @playwright/test defaults and
  rejects invalid values on construction.
A guide on why and where to compare screenshots (a defensive test on the
design system and precious pages), how baselines are named, updated and
reviewed, failure images, PNG and WebP, options, headless mode and limits.
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@smnandre
smnandre requested a balanced review from Copilot September 26, 2026 22:24
@smnandre smnandre added the enhancement New feature or request label Sep 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Empty baselines can pass incorrectly, stale artifacts can persist, and several path, locator, and portability cases remain broken.

Review effort: Balanced
Findings: 1 High severity · 3 Medium severity · 1 Low severity

Open (5)
What changed in this PR

Adds visual regression assertions for pages and locators, backed by Playwright’s screenshot comparator.

Changes:

  • Adds PNG/WebP screenshot comparison, update modes, masks, tolerances, and failure artifacts.
  • Integrates assertions with PHPUnit tracing, snapshot paths, and browser/platform variants.
  • Adds documentation and unit/functional coverage.
File Description
bin/​lib/​handlers.js Bridges screenshot comparisons to Playwright.
src/​Assertions/​Internal/​AbstractAssertions.php Supports non-polled assertions.
src/​Assertions/​Internal/​ScreenshotExpectation.php Manages baselines and artifacts.
src/​Assertions/​LocatorAssertions.php Adds locator screenshot assertions.
src/​Assertions/​LocatorAssertionsInterface.php Exposes the locator API.
src/​Assertions/​Options/​ToHaveScreenshotOptions.php Defines and validates options.
src/​Assertions/​PageAssertions.php Adds page screenshot assertions.
src/​Assertions/​PageAssertionsInterface.php Exposes the page API.
src/​Locator/​Locator.php Sends locator comparison commands.
src/​Page/​Page.php Sends page comparison commands.
src/​Screenshot/​ScreenshotComparison.php Models comparison results.
src/​Testing/​Expect.php Resolves snapshot paths and variants.
src/​Testing/​ExpectDecorator.php Counts screenshot assertions.
src/​Testing/​ExpectInterface.php Exposes the testing API.
src/​Testing/​PlaywrightTestCaseTrait.php Configures baseline and output directories.
tests/​Functional/​Screenshot/​ToHaveScreenshotTest.php Exercises browser-level behavior.
tests/​Unit/​Assertions/​Internal/​ScreenshotExpectationTest.php Tests baseline lifecycle.
tests/​Unit/​Assertions/​LocatorAssertionsTest.php Tests locator integration.
tests/​Unit/​Assertions/​Options/​ToHaveScreenshotOptionsTest.php Tests option validation.
tests/​Unit/​Assertions/​PageAssertionsTest.php Tests page integration.
tests/​Unit/​Locator/​LocatorTest.php Tests locator transport behavior.
tests/​Unit/​Page/​PageTest.php Tests page transport behavior.
tests/​Unit/​Screenshot/​ScreenshotComparisonTest.php Tests response decoding.
tests/​Unit/​Testing/​ExpectFactoryTest.php Tests paths and PHPUnit integration.
docs/​guide/​assertions-reference.md Lists the new assertions.
docs/​guide/​visual-regression.md Documents visual regression workflows.
CHANGELOG.md Announces the feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Testing/Expect.php
Comment on lines +208 to +213
try {
$page = $subject instanceof LocatorInterface ? $subject->page() : $subject;

return $page->context()->browser()?->browserType()->value ?? '';
} catch (RuntimeException) {
return '';
*/
private function writeArtifacts(string $path, string $expected, ScreenshotComparison $result): array
{
$stem = $this->artifactStem($path);
Comment thread src/Testing/Expect.php Outdated
Comment on lines +191 to +193
if (str_starts_with($name, '/') || 1 === preg_match('~^[a-zA-Z]:[\\\\/]~', $name)) {
return $name;
}
Comment on lines +417 to +419
if (0 === posix_geteuid()) {
$this->markTestSkipped('Root reads files whatever their permissions.');
}
* Rebinds $this->locator to the '.items' selector used by the filter and
* combinator tests, which need a different base selector than setUp().
*/
public function testExpectScreenshotSendsTheBaselineAndOptions(): void
- An empty baseline reached Playwright as "no baseline" and matched any
  screenshot. It now fails with a clear message, and is recorded again in
  the changed and all modes. The bridge sends any string it receives.
- A failure removes the images of the previous one, so a diff from an
  earlier mismatch never sits next to a later timeout.
- Locator::normalize() keeps the page of the locator, so a normalized
  locator names its baselines after the browser too.
- UNC paths (\\server\share\card.png) are absolute snapshot paths.
- The unreadable baseline test skips without POSIX permissions.
- The useItemsLocator() docblock is back on its helper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants