Przejdź do treści

Open source · MIT · Cypress

Regresja wizualna dla Cypressa.

Zrób zrzut ekranu w dowolnym teście, porównaj go ze wzorcem i zobacz różnicę prosto w runnerze Cypressa. Działa z testami e2e i komponentowymi.

npm i -D @pixsame/cypress-plugin-visual-regression-diff
Czytaj dokumentację →Changelog →Migracja z @frsource →npm ↗

Ta strona to tłumaczenie maszynowe, które nie zostało jeszcze zweryfikowane. Wersja angielska jest wiążąca.

Pierwsze kroki

Instalacja

Plugin wymaga Node.js 20.9 lub nowszego. Bibliotekę zainstalujesz swoim ulubionym menedżerem pakietów:

# npm
npm install --save-dev @pixsame/cypress-plugin-visual-regression-diff

# yarn
yarn add -D @pixsame/cypress-plugin-visual-regression-diff

# pnpm
pnpm add -D @pixsame/cypress-plugin-visual-regression-diff

Następnie musisz zaimportować bibliotekę:

  • najpierw w pliku support (domyślnie znajduje się w cypress/support/index.js):
// typescript / ES6
import '@pixsame/cypress-plugin-visual-regression-diff';

// javascript
require('@pixsame/cypress-plugin-visual-regression-diff');
  • a potem w cypress.config.js (lub cypress.config.ts):
// typescript / ES6
import { defineConfig } from 'cypress';
import { initPlugin as initVisualRegressionPlugin } from '@pixsame/cypress-plugin-visual-regression-diff/plugins';

export default defineConfig({
  // initPlugin trzeba wywołać w tej sekcji, w której jest używany: e2e lub component
  e2e: {
    setupNodeEvents(on, config) {
      initVisualRegressionPlugin(on, config);
    },
  },
  component: {
    setupNodeEvents(on, config) {
      initVisualRegressionPlugin(on, config);
    },
  },
});

To wszystko - teraz zobaczmy, jak korzystać z biblioteki, w sekcji o użyciu.

Użycie

Po instalacji wystarczy, że w teście napiszesz:

cy.get('.an-element-of-your-choice').matchImage();

Albo, jeśli chcesz zrobić zrzut ekranu całego dokumentu:

cy.matchImage();

Komenda matchImage robi zrzut ekranu i porównuje go z obrazem z poprzedniego uruchomienia. W razie regresji test nie przejdzie, a ty dostaniesz przycisk "See comparison", który pokaże, gdzie leży problem.

Przykład

Nadal masz problemy z instalacją? Zajrzyj do katalogu examples w tym repozytorium, żeby zobaczyć, jak użyć tego pluginu w środowisku Cypress przy testach e2e lub komponentowych w twoim projekcie.

Automatyczne usuwanie nieużywanych obrazów

Warto usuwać zrzuty ekranu wygenerowane przez plugin, z których nie korzysta już żaden test. Włącz tę funkcję przez zmienną expose i ciesz się odzyskanym miejscem na dysku 🚀:

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionCleanupUnusedImages=true"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionCleanupUnusedImages=true"

Manifest uruchomienia (integracja z CI)

Od wersji 1.1 każde uruchomienie zapisuje manifest JSON z listą wszystkich porównań matchImage: z którego testu pochodzi dane porównanie, czy przeszło, oblało, utworzyło lub zaktualizowało swój baseline, jaki był współczynnik diffa oraz ścieżki (względem projektu) do plików baseline, .actual.png i .diff.png. Obok wpisów zapisuje też, gdzie odbyło się uruchomienie (dostawca CI, repozytorium, commit, pull request, id uruchomienia) i z czym zostało uruchomione (przeglądarka, pliki spec, plik konfiguracyjny, opcje pluginu), dzięki czemu narzędzie czytające plik może zaraportować wynik do właściwego PR-a, pobrać właściwy artefakt albo ponownie uruchomić te same porównania. Manifest jest przeznaczony dla narzędzi CI (komentarze w PR-ach, dashboardy do przeglądu, boty zatwierdzające), które działają po zakończeniu Cypressa.

Format to otwarty, niezależny od runnera standard oparty na CTRF i udokumentowany na visual-regression.dev/manifest. Pakiet npm visual-regression-manifest dostarcza JSON Schema, typy TypeScript, walidujący reader, merger dla manifestów z całego uruchomienia oraz konwertery dla narzędzi, które nie piszą własnego manifestu (toHaveScreenshot z Playwrighta, Vitest, jest-image-snapshot, BackstopJS, raporty CTRF, zwykłe pliki obrazów). Swoje narzędzia buduj na tym pakiecie, a nie na tym pluginie.

Domyślnie plik ląduje obok pozostałych artefaktów Cypressa, w <screenshotsFolder>/visual-regression-manifest.<testingType>.json (np. cypress/screenshots/visual-regression-manifest.e2e.json). Wgraj go razem z katalogiem snapshotów jako artefakt CI.

{
  "reportFormat": "visual-regression-manifest",
  "specVersion": "1.0.0-beta.1",
  "createdAt": "2026-09-22T10:00:00.000Z",
  "updatedAt": "2026-09-22T10:03:12.481Z",
  "projectRoot": "/home/runner/work/app/app",
  "platform": {
    "osPlatform": "linux",
    "arch": "x64",
    "osRelease": "6.8.0-1021-azure",
  },
  "ci": {
    "provider": "github",
    "repositoryName": "acme/app",
    "repositoryUrl": "https://github.com/acme/app",
    "commit": "0f1e2d…", // przy zdarzeniach pull_request: commit merge
    "ref": "refs/pull/42/merge",
    "branchName": "feat/new-header",
    "pullRequest": {
      "number": 42,
      "headSha": "9a8b7c…",
      "headRef": "feat/new-header",
      "baseRef": "main",
    },
    "event": "pull_request",
    "buildId": "1234567890",
    "buildName": "CI",
    "buildUrl": "https://github.com/acme/app/actions/runs/1234567890/attempts/1",
    "runAttempt": "1",
    "job": "e2e",
    "workspace": "/home/runner/work/app/app",
  },
  "tool": {
    "name": "@pixsame/cypress-plugin-visual-regression-diff",
    "version": "1.1.0",
  }, // co zapisało plik
  "options": { "updateImages": "failures-only" }, // globalne opcje pluginVisualRegression*, bez prefiksu
  "runner": {
    "name": "cypress",
    "version": "16.1.0",
    "testingType": "e2e",
    "mode": "run",
    "configFile": "cypress.config.ts",
    "browser": {
      "name": "chrome",
      "version": "130.0.0.0",
      "family": "chromium",
      "headless": true,
    },
    "specs": ["cypress/e2e/home.cy.ts"],
    "specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
    "baseUrl": "http://localhost:3000",
    "viewport": { "width": 1280, "height": 720 },
  },
  "summary": {
    "entries": 1,
    "passed": 0,
    "failed": 1,
    "missingBaseline": 0,
    "created": 0,
    "updated": 0,
    "approved": 0,
    "needsHuman": 1,
  },
  "entries": [
    {
      "name": "home page renders_#0",
      "test": {
        "filePath": "cypress/e2e/home.cy.ts",
        "titlePath": ["home page", "renders"],
        "retry": 0,
      },
      "status": "failed",
      "comparison": { "diffRatio": 0.0231, "threshold": 0.01 },
      "images": {
        "baseline": {
          "path": "cypress/e2e/__image_snapshots__/home page renders_#0.png",
          "width": 1280,
          "height": 720,
        },
        "actual": {
          "path": "cypress/e2e/__image_snapshots__/home page renders_#0.actual.png",
          "width": 1280,
          "height": 720,
        },
        "diff": {
          "path": "cypress/e2e/__image_snapshots__/home page renders_#0.diff.png",
        },
      },
      "baselineWritten": false,
      "recordedAt": "2026-09-22T10:03:12.480Z",
      "platform": {
        "osPlatform": "linux",
        "arch": "x64",
        "browser": {
          "name": "chrome",
          "version": "130.0.0.0",
          "family": "chromium",
          "headless": true,
        },
      },
      "viewport": { "width": 1280, "height": 720 },
      "options": {
        "imagesPath": "{spec_path}/__image_snapshots__",
        "maxDiffThreshold": 0.01,
        "diffConfig": {},
        "createMissingImages": true,
        "updateImages": false,
        "forceDeviceScaleFactor": true,
        "screenshotConfig": {},
      },
      "renderer": {
        "backend": "native",
        "browser": "chrome",
        "browserVersion": "130.0.0.0",
      },
      "hashes": {
        "baseline": "3b7e…", // sha256 plików wymienionych w `images`, tylko dla plików, które istnieją
        "actual": "9d21…",
        "diff": "c0ff…",
      },
      "message": "2310 pixels (2.31%) differ; the limit is 1%.",
    },
  ],
}
statusznaczenieimages.actual.pathimages.diff.path
passedw granicach progunullnull
failedpowyżej progu, .actual.png i .diff.png zostają do przegląduścieżkaścieżka
missing-baselinebrak baseline'u i createMissingImages: falseścieżkanull
createdbrak baseline'u, .actual.png stał się baseline'emnullnull
updatedbaseline nadpisany (updateImages: true lub 'failures-only')nullnull
approvedbaseline zastąpiony z UI przeglądu w trybie headed ("Replace image")nullnull

Pola na poziomie uruchomienia:

poleznaczenie
createdAt, updatedAtkiedy uruchomienie się zaczęło i kiedy plik został ostatnio przepisany (ISO 8601)
platformosPlatform (linux, darwin, win32), arch, osRelease jądra oraz, w trybie run, czytelna dla człowieka osVersion maszyny, która uruchomiła testy
toolco zapisało plik: ten plugin i jego wersja
ciwykrywane ze zmiennych środowiskowych na GitHub Actions i GitLab CI; { "provider": null } na innych systemach CI (ustawione jest tylko CI), null poza CI. Ze środowiska kopiowane są wyłącznie wymienione klucze
optionskażda opcja pluginVisualRegression* z expose/env, bez prefiksu, wartości takie, jak skonfigurowano (wartości z CLI pozostają stringami)
runnerco uruchomiło testy. mode to run lub open; browser to uruchomiona przeglądarka; specs jest znane tylko w trybie run; configFile i specs są względne wobec projectRoot. Uruchomienia w Cypress Cloud dostają też cloud.runUrl, group, tag, parallel
summaryliczba wpisów dla każdego statusu; needsHuman to failed + missingBaseline, więc konsument może podjąć decyzję bez czytania wpisów

Uwagi dla konsumentów:

  • Wszystkie ścieżki są względne wobec projectRoot i używają separatora /; mogą wskazywać poza projekt (../…), gdy imagesPath jest bezwzględna. projectRoot to projekt Cypressa, niekoniecznie katalog główny repozytorium: ci.workspace to katalog checkoutu, więc relative(ci.workspace, projectRoot) daje prefiks ścieżki wewnątrz repozytorium.
  • failed i missing-baseline to wpisy, które wymagają człowieka: skopiowanie images.actual.path w miejsce images.baseline.path je zatwierdza.
  • baselineWritten mówi, czy drzewo robocze zmieniło się dla danego zrzutu ekranu, niezależnie od status.
  • Wpis, którego porównanie wykryło różnice, zawiera extra.pixsame.analysis (o ile opcja analysis nie jest wyłączona): label (content-change, moved, noise-like lub size-change), jednozdaniowy description, zmienione obszary regions wraz z ich ramkami, wykryte przesunięcie shift oraz bundleKey, który jest taki sam dla wpisów zmienionych w ten sam sposób. Funkcja bundle() z @pixsame/analysis grupuje wpisy według niego.
  • Każdy wpis niesie własne platform i viewport, więc po sklejeniu kilku manifestów da się odróżnić wpisy z macierzy maszyn. options we wpisie to dokładne wejście matchImage (imagesPath zachowuje nierozwinięte tokeny); comparison to wynik.
  • platform to miejsce, gdzie uruchomił się test, renderer to miejsce, skąd pochodzą piksele. Dziś każdy zrzut ekranu robi przeglądarka sterowana przez Cypressa, więc renderer.backend to native, a renderer.browser powtarza platform.browser.name. Pole istnieje po to, żeby renderer inny niż lokalna przeglądarka (przypięty obraz Dockera, hostowany renderer) mógł się zidentyfikować przez rendererVersion i imageDigest, a uruchomienie, które musiało wrócić do lokalnej przeglądarki, mogło zostać oznaczone renderer.fallback: true. Wpisy z różnymi rendererami traktuj jako różne baseline'y.
  • hashes zawiera sha256 każdego pliku z images, który istnieje na dysku. Użyj ich, żeby pominąć ponowne wgrywanie niezmienionych baseline'ów, sprawdzić, czy artefakt zgadza się z manifestem, albo wykryć dwa wpisy, które dały identyczne piksele.
  • Mapowanie na pull requesta: użyj ci.repositoryName i ci.pullRequest.number. Przy zdarzeniach pull_request na GitHubie ci.commit to tymczasowy commit merge, więc wszystko, co pushuje do gałęzi PR-a, musi używać ci.pullRequest.headSha / headRef. Artefakt workflow znajdziesz przez ci.buildId i ci.runAttempt.
  • Odtworzenie uruchomienia: npx cypress run --<runner.testingType> --browser <runner.browser.name> --config-file <runner.configFile> --spec <runner.specs joined with ,>, plus jedno --expose "pluginVisualRegression<Key>=<value>" na każdy wpis z options. Manifest przechowuje je jako osobne części, a nie jako string polecenia, więc cytowanie w shellu i menedżer pakietów pozostają twoim wyborem.
  • Plik jest przepisywany po każdym porównaniu, więc jest kompletny nawet wtedy, gdy uruchomienie zostanie przerwane. Gdy pliki spec działają równolegle na kilku maszynach, każda maszyna zapisuje własny manifest; **/visual-regression-manifest{,.*}.json (MANIFEST_FILE_GLOB w pakiecie) znajduje je wszystkie w artefakcie, a readManifestFiles + mergeManifests z visual-regression-manifest łączą je w jedną listę, w której późniejsza próba CI wygrywa z wcześniejszą.
  • Tylko blok runner i klucze options są specyficzne dla Cypressa; reszta formatu jest wspólna dla wszystkich pozostałych narzędzi zapisujących manifest. specVersion trzyma się semver, a jego major jest podbijany wyłącznie przy łamiących zmianach formatu; konsumenci ignorują klucze, których nie znają. Ponieważ nazwy pól są zgodne z CTRF, toCtrf z pakietu zamienia manifest w raport CTRF dla narzędzi, które czytają ten format.
  • Typy Manifest* są dla wygody re-eksportowane z @pixsame/cypress-plugin-visual-regression-diff/plugins; to te same typy, co w visual-regression-manifest.

Manifest jest domyślnie włączony. Żeby zapisać go gdzie indziej (ścieżki są rozwiązywane względem katalogu głównego projektu) albo go wyłączyć, użyj klucza pluginVisualRegressionManifestPath (false go wyłącza, nic innego się nie zmienia):

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionManifestPath=reports/visual-regression.json"
npx cypress run --expose "pluginVisualRegressionManifestPath=false"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionManifestPath=false"

Gdy z własną ścieżką uruchamiasz zarówno testy e2e, jak i komponentowe, sam dodaj do niej typ testów; w przeciwnym razie uruchomienia nadpisywałyby sobie nawzajem plik.

Konfiguracja

Plugin skonfigurujesz:

  • przekazując konfigurację jako argument komendy matchImage:
cy.matchImage({
  // konfiguracja zrzutu ekranu, przekazywana bezpośrednio do metody screenshot w Cypress: https://docs.cypress.io/api/cypress-api/screenshot-api#Arguments
  // domyślnie: { }
  screenshotConfig: {
    blackout: ['.element-to-be-blackouted'],
  },
  // opcje pixelmatch, zobacz: https://www.npmjs.com/package/pixelmatch#pixelmatchimg1-img2-output-width-height-options
  // domyślnie: {} - domyślne ustawienia pixelmatch, w tym includeAA: false (piksele krawędzi z anti-aliasingiem nie są liczone jako różnice)
  diffConfig: {
    threshold: 0.01,
  },
  // czy automatycznie tworzyć brakujące obrazy baseline
  // domyślnie: true
  createMissingImages: false,
  // czy automatycznie aktualizować obrazy - przydatne na CI
  // - true: nadpisuj wszystkie baseline'y bez porównywania (najszybsze)
  // - 'failures-only': najpierw porównaj, aktualizuj baseline tylko wtedy, gdy diff przekroczy próg
  // - false: nigdy nie aktualizuj baseline'ów automatycznie
  // domyślnie: false
  updateImages: true,
  // ścieżka katalogu, w którym będą zapisywane zrzuty ekranu
  // ścieżki względne są rozwiązywane względem katalogu głównego projektu
  // ścieżki bezwzględne (zarówno na unixie, jak i na Windowsie) są obsługiwane
  // separatory ścieżek plugin znormalizuje zależnie od systemu; zawsze używaj / jako separatora, np.: C:/my-directory/nested dla notacji dyskowej w stylu Windows
  // w ścieżce można użyć tych zmiennych:
  // - {spec_path} - ścieżka względna od katalogu głównego projektu do katalogu bieżącego pliku spec (np. `/src/components/my-tested-component`); musi stanowić cały segment ścieżki
  // - {platform} - `{os}-{browser}`, np. `linux-chrome`, `darwin-electron`, `win32-firefox`
  // - {os} - Cypress.platform: `linux`, `darwin` albo `win32`
  // - {browser} - przeglądarka, w której Cypress uruchamia spec (Cypress.browser.name): `electron`, o ile `--browser` nie mówi inaczej, `chrome`, `firefox`, `edge`, ...
  // {platform}, {os} i {browser} mogą też być częścią segmentu (`shots-{platform}`); `matchAgainstPath` nigdy nie jest rozwijana
  // używaj ich, żeby trzymać osobno baseline'y z różnych systemów/przeglądarek, zobacz "Osobne baseline'y dla każdej platformy" poniżej
  // domyślnie: '{spec_path}/__image_snapshots__'
  imagesPath: 'this-might-be-your-custom/maybe-nested-directory',
  // maksymalny próg, powyżej którego test ma nie przejść
  // domyślnie: 0.01
  maxDiffThreshold: 0.1,
  // opisuje, co się zmieniło, gdy porównanie wykryje różnice, np. "Everything below y=340 moved down by 12 px."
  // zdanie kończy komunikat błędu, jest widoczne w karuzeli przeglądu i trafia do manifestu przebiegu
  // nigdy nie wpływa na to, czy porównanie przechodzi
  // domyślnie: true
  analysis: false,
  // wymusza ustawienie współczynnika skalowania na wartość "1"
  // pomaga, gdy zrzuty ekranu są skalowane 2x na ekranach o wysokiej gęstości, np. Mac Retina
  // domyślnie: true
  forceDeviceScaleFactor: false,
  // włącza preset deterministycznego renderowania, zobacz "Reducing cross-OS rendering noise" poniżej
  // dla pojedynczego wywołania kontroluje tylko CSS wstrzykiwany do strony na czas zrzutu ekranu (kursor, animacje, paski przewijania);
  // przełączniki przeglądarki działają na poziomie całego procesu i zależą od globalnej opcji pluginVisualRegressionDeterministicRendering
  // domyślnie: true
  deterministicRendering: false,
  // tytuł używany do nazwania pliku obrazu
  // domyślnie: Cypress.currentTest.titlePath (tytuł twojego testu)
  title: `${Cypress.currentTest.titlePath.join(' ')} (${Cypress.browser.displayName})`,
  // ścieżka do własnego obrazu, z którym ma być robione porównanie
  // zamiast porównywania z obrazem z poprzedniego uruchomienia
  // domyślnie: undefined
  matchAgainstPath: '/path/to/reference-image.png',
});
  • przez globalną konfigurację expose (Cypress 15.10+). Nazwy kluczy konfiguracji są takie same jak klucze obiektu konfiguracji powyżej, tylko z dodanym prefiksem pluginVisualRegression, np.:
# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionUpdateImages=true,pluginVisualRegressionDiffConfig={\"threshold\":0.01}"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionUpdateImages=true,pluginVisualRegressionDiffConfig={\"threshold\":0.01}"

Kilka opcji przekazuj w jednej fladze, rozdzielone przecinkami. Gdy --expose (lub --env) powtórzy się, Cypress weźmie pod uwagę tylko ostatnie wystąpienie.

// cypress.config.ts (Cypress 15.10+)
import { defineConfig } from 'cypress';

export default defineConfig({
  expose: {
    pluginVisualRegressionUpdateImages: true,
    pluginVisualRegressionDiffConfig: { threshold: 0.01 },
  },
});
// cypress.config.ts (Cypress <15.10, przestarzałe w nowszych wersjach)
import { defineConfig } from 'cypress';

export default defineConfig({
  env: {
    pluginVisualRegressionUpdateImages: true,
    pluginVisualRegressionDiffConfig: { threshold: 0.01 },
  },
});

Więcej sposobów ustawiania zmiennych expose znajdziesz tutaj.

Osobne baseline'y dla każdej platformy

Chrome zostawia rasteryzację tekstu systemowi operacyjnemu, więc ta sama strona nigdy nie renderuje się bajt w bajt identycznie na macOS, Linuksie i Windowsie, a do tego przeglądarki różnią się jeszcze między sobą. Baseline nagrany na Macu developera rzadko więc pasuje do zrzutu ekranu tej samej strony zrobionego przez linuksowy job CI. imagesPath rozumie trzy tokeny, które trzymają baseline'y różnych platform osobno, zamiast pozwalać im się wzajemnie nadpisywać:

tokenrozwija się do
{os}Cypress.platform: linux, darwin albo win32
{browser}Cypress.browser.name, czyli przeglądarka, w której Cypress uruchamia spec: electron, o ile nie przekażesz --browser, w przeciwnym razie chrome, firefox, edge, ...
{platform}{os}-{browser}, np. linux-chrome, darwin-electron

Każdy zrzut ekranu robi przeglądarka sterowana przez Cypressa, więc {browser} to właśnie ta przeglądarka; cypress run bez --browser używa Electrona, dlatego katalog nazywa się darwin-electron, a nie darwin-chrome, chyba że CI przekaże --browser chrome.

Tokeny są opcjonalne: domyślny imagesPath to nadal {spec_path}/__image_snapshots__, czyli jeden zestaw baseline'ów dla wszystkich. To właściwy układ, gdy każde uruchomienie odbywa się na tym samym systemie i w tej samej przeglądarce (na przykład w kontenerze, zobacz FAQ) albo gdy pozostałe różnice mieszczą się w twoim maxDiffThreshold. Żeby podzielić baseline'y według platformy, dodaj token globalnie:

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionImagesPath={spec_path}/__image_snapshots__/{platform}"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionImagesPath={spec_path}/__image_snapshots__/{platform}"
// cypress.config.ts (Cypress 15.10+; na Cypress <15.10 użyj `env` zamiast `expose`)
export default defineConfig({
  expose: {
    pluginVisualRegressionImagesPath:
      '{spec_path}/__image_snapshots__/{platform}',
  },
});
cypress/e2e/__image_snapshots__/
├── darwin-electron/   # to, co `cypress open` zapisuje na Macu
│   └── home page renders_#0.png
└── linux-chrome/      # to, co zapisuje twój job CI
    └── home page renders_#0.png

Co trzeba ustalić po podziale:

  • Które baseline'y commitować. Większość zespołów commituje tylko platformę CI, a lokalne baseline'y trzyma poza gitem, dzięki czemu każdy developer ma własny zestaw bez zaśmiecania repozytorium:
    # ignoruj wszystkie katalogi platform poza tym, który produkuje CI
    **/__image_snapshots__/*/
    !**/__image_snapshots__/linux-chrome/
    

    Nowe baseline'y CI trzeba wtedy wygenerować na CI i stamtąd je zacommitować (albo zatwierdzić z poziomu PR-a, zobacz Manifest uruchomienia).
  • Podział tylko według przeglądarki albo tylko według systemu. Użyj samego {browser} albo samego {os}, np. {spec_path}/__image_snapshots__/{browser}.
  • Czyszczenie usuwa baseline'y pozostałych platform. pluginVisualRegressionCleanupUnusedImages przeszukuje cały projekt w poszukiwaniu PNG-ów stworzonych przez plugin i nic nie wie o platformach: uruchomienie na darwin-electron traktuje każdy plik w linux-chrome/ jako nieużywany i go usuwa. Trzymaj tę opcję wyłączoną na maszynach, które mają katalogi więcej niż jednej platformy (checkout developera z zacommitowanymi baseline'ami CI to dokładnie taki przypadek), a włączaj ją tylko tam, gdzie leżą baseline'y jednej platformy, zwykle w jobie CI, który je produkuje. Jeśli jednak coś zostanie usunięte, skasowane katalogi wrócą po git checkout -- <folder>.

Dokąd to zmierza: plan dla #212 to renderer, który produkuje piksele w przypiętym obrazie Dockera niezależnie od tego, gdzie działa Cypress, więc obrazy lokalne i z CI przestają się rozjeżdżać z samej konstrukcji. {browser} nazywa wtedy przeglądarkę renderera, a nie tę sterowaną przez Cypressa, a {platform} nadal przydaje się do trzymania osobno kilku renderowanych przeglądarek, a nie do różnic lokalne-vs-CI. Manifest już zapisuje obie strony: platform to miejsce, gdzie uruchomiono test, renderer to miejsce, skąd pochodzą piksele.

Tryb Batch Review

Tryb Batch Review jest domyślnie włączony. Pozwala uruchomić cały zestaw testów bez zatrzymywania się na pierwszym nieudanym porównaniu wizualnym. Zamiast tego wszystkie nieudane snapshoty są zbierane i po zakończeniu testów prezentowane w interaktywnym UI do przeglądu, dzięki czemu zmiany możesz zatwierdzać lub pomijać hurtowo.

Dodatki w trybie headed (środowiska poza CI)

W prawym dolnym rogu runnera Cypress pojawia się pływający przycisk akcji (FAB). W trakcie działania testów:

  • Za każdym razem, gdy wywołanie matchImage() przekroczy próg diffa, licznik na FAB-ie się zwiększa, zamiast od razu rzucać błąd.
  • Gdy wszystkie testy się skończą, rzucany jest błąd z łączną liczbą niepowodzeń.
  • Kliknięcie FAB-a otwiera karuzelę, w której przejrzysz każdy nieudany snapshot obok siebie (nowy vs. stary) i albo zastąpisz baseline, albo pominiesz zmianę.

Demo Trybu Batch Review

Uwaga: przed wersją 2 pakietu @pixsame/cypress-plugin-visual-regression-diff tryb Batch Review był opt-in. Zobacz przewodnik migracji, jeśli aktualizujesz z 4.x.

Jak wyłączyć

Tryb Batch Review to opcja globalna (nie da się jej przekazać bezpośrednio do matchImage). Żeby wrócić do natychmiastowego oblewania matchImage() przy pierwszej różnicy, ustaw klucz pluginVisualRegressionBatchReviewMode na false:

# Cypress 15.10+
npx cypress open --expose "pluginVisualRegressionBatchReviewMode=false"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress open --env "pluginVisualRegressionBatchReviewMode=false"
// cypress.config.ts (Cypress 15.10+)
import { defineConfig } from 'cypress';

export default defineConfig({
  expose: {
    pluginVisualRegressionBatchReviewMode: false,
  },
});
// cypress.config.ts (Cypress <15.10, przestarzałe w nowszych wersjach)
import { defineConfig } from 'cypress';

export default defineConfig({
  env: {
    pluginVisualRegressionBatchReviewMode: false,
  },
});

Przeglądanie obrazów, które przeszły

Obrazy, które różnią się od baseline'u, ale mieszczą się w maxDiffThreshold, nie oblewają testu, a mimo to są zbierane. Karuzela ma checkbox Show passing, który pozwala je przejrzeć (każdy ma odznakę passed) i również zastąpić ich baseline, np. żeby pozbyć się drobnego, nawarstwiającego się dryfu.

Checkbox jest domyślnie odznaczony. Użyj klucza pluginVisualRegressionBatchReviewModeShowPassingImages, żeby był domyślnie zaznaczony:

# Cypress 15.10+
npx cypress open --expose "pluginVisualRegressionBatchReviewModeShowPassingImages=true"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress open --env "pluginVisualRegressionBatchReviewModeShowPassingImages=true"
// cypress.config.ts (Cypress 15.10+)
import { defineConfig } from 'cypress';

export default defineConfig({
  expose: {
    pluginVisualRegressionBatchReviewModeShowPassingImages: true,
  },
});

Przełączenie checkboxa w runnerze jest zapamiętywane w local storage przeglądarki i przy kolejnych uruchomieniach ma pierwszeństwo przed wartością z konfiguracji.

Reducing cross-OS rendering noise

Przeglądarki oddają rasteryzację tekstu systemowi operacyjnemu (CoreText na macOS, DirectWrite na Windows, FreeType na Linuksie), więc ta sama strona nigdy nie wyrenderuje się bajt w bajt identycznie na laptopie developera i na linuksowym runnerze CI. Plugin nie jest w stanie całkowicie tego wyeliminować, ale preset deterministicRendering (domyślnie włączony) usuwa tę część szumu, która leży po stronie samej przeglądarki:

  • Chrome, Chromium, Edge są uruchamiane z --font-render-hinting=none --disable-font-subpixel-positioning --disable-lcd-text --force-color-profile=srgb --disable-gpu (tekst bez hintingu systemowego i sztuczek subpikselowych, bez zarządzania kolorem, rasteryzacja na CPU), a w trybie headless dodatkowo --hide-scrollbars. Lista jest eksportowana jako DETERMINISTIC_RENDERING_CHROMIUM_ARGS z @pixsame/cypress-plugin-visual-regression-diff/constants.
  • Firefox dostaje preferencję gfx.webrender.software (renderowanie na CPU). Firefox nie ma międzyplatformowego przełącznika hintingu fontów, więc tekst może nadal różnić się między systemami operacyjnymi.
  • Każda przeglądarka: na czas zrzutu ekranu z matchImage do testowanej strony wstrzykiwany jest element <style>, który ukrywa kursor tekstowy, wyłącza przejścia CSS, animacje i płynne przewijanie oraz ukrywa paski przewijania. Zaraz po zrzucie jest usuwany.
  • Porównanie uruchamia pixelmatch z includeAA: false, więc piksele krawędzi z anti-aliasingiem są wykrywane i nie są liczone jako różnice.

Czego preset nie rozwiązuje: różnych plików fontów i fallbacków na poszczególnych systemach (klasyczny przypadek Arial vs. Liberation Sans), fontów emoji, rasteryzacji tekstu w WebKit i Firefoxie oraz obrazów rysowanych przez WebGL. W takich przypadkach trzymaj osobny baseline dla każdej platformy (zobacz opcje title i imagesPath oraz wpis w FAQ o nazwach przeglądarek) albo generuj baseline'y w tym samym środowisku, w którym działa twoje CI.

Electron ignoruje przełączniki ustawiane przez pluginy. Przekaż je zamiast tego przez zmienną środowiskową:

ELECTRON_EXTRA_LAUNCH_ARGS="--font-render-hinting=none --disable-font-subpixel-positioning --disable-lcd-text --force-color-profile=srgb --disable-gpu --hide-scrollbars" npx cypress run

WebGL: --disable-gpu może sprawić, że strony zależne od WebGL zostaną bez kontekstu renderowania. Albo wyłącz preset, albo dodaj --use-gl=angle --use-angle=swiftshader (nowsze Chrome wymaga też --enable-unsafe-swiftshader) we własnym handlerze before:browser:launch, połączonym z handlerem pluginu przez cypress-on-fix.

Hooki zrzutów ekranu: plugin przekazuje własne onBeforeScreenshot i onAfterScreenshot do każdego zrzutu, który robi. Hooki podane przez screenshotConfig są nadal wywoływane, a hooki ustawione globalnie przez Cypress.Screenshot.defaults() nie.

Żeby wyłączyć preset globalnie (przełączniki przeglądarki i CSS):

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionDeterministicRendering=false"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionDeterministicRendering=false"
// cypress.config.ts (Cypress 15.10+)
import { defineConfig } from 'cypress';

export default defineConfig({
  expose: {
    pluginVisualRegressionDeterministicRendering: false,
  },
});

Żeby zachować przełączniki przeglądarki, ale pominąć CSS dla pojedynczego zrzutu (np. gdy chcesz złapać stan końcowy animacji):

cy.matchImage({ deterministicRendering: false });

FAQ

Dlaczego zrzuty ekranu nie trzymają się viewport ustawionego w mojej konfiguracji Cypress?

Zrzuty ekranu w Cypress domyślnie nie skalują się do rozmiaru viewportu. Możesz to zmienić:

  • globalnie, zmieniając domyślną konfigurację zrzutów ekranu: Cypress.Screenshot.defaults({ capture: 'viewport' });
  • lokalnie, przekazując konfigurację zrzutu ekranu bezpośrednio do komendy .matchImage: cy.matchImage({ screenshotConfig: { capture: 'viewport' } });

Zaktualizowałem wersję pluginu i wszystkie moje obrazy baseline zostały automatycznie podmienione. Dlaczego?

Czasem musimy wprowadzić zmianę łamiącą kompatybilność w algorytmach porównywania lub generowania obrazów. Żeby aktualizacja była dla ciebie jak najprostsza, plugin sam aktualizuje twoje baseline'y. Po aktualizacji pluginu po prostu zacommituj je do repozytorium i gotowe!

Zrzuty ekranu wyglądają inaczej w cypress run i cypress open. Jak to naprawić?

Zwykle wynika to z różnic w device pixel ratio między trybem headless a headed. Użyj opcji forceDeviceScaleFactor, żeby znormalizować współczynnik skalowania do 1:

cy.matchImage({ forceDeviceScaleFactor: true });

Albo ustaw to globalnie przez zmienną expose:

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionForceDeviceScaleFactor=true"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionForceDeviceScaleFactor=true"

Jeśli różnice w rozmiarze viewportu nie znikają, rozważ jawne ustawienie rozmiaru okna przeglądarki w setupNodeEvents (połącz to z własnym handlerem before:browser:launch pluginu przez cypress-on-fix):

on('before:browser:launch', (browser, launchOptions) => {
  if (browser.family === 'chromium' && browser.isHeadless) {
    launchOptions.args.push('--window-size=1280,720');
  }
  return launchOptions;
});

Jeśli oba uruchomienia odbywają się na różnych systemach operacyjnych (laptop z macOS vs. linuksowy runner CI), pozostałe różnice wynikają z renderowania fontów. Zobacz Reducing cross-OS rendering noise.

Jak używać tego pluginu razem z innymi pluginami Cypress, które też rejestrują zdarzenia w setupNodeEvents?

Cypress obsługuje tylko jeden handler na zdarzenie. Jeśli kilka pluginów zarejestruje to samo zdarzenie (np. after:screenshot), wywołany zostanie tylko ostatni z nich. Żeby bezpiecznie połączyć kilka pluginów, użyj cypress-on-fix:

import { defineConfig } from 'cypress';
import { initPlugin as initVisualRegressionPlugin } from '@pixsame/cypress-plugin-visual-regression-diff/plugins';
import fix from 'cypress-on-fix';

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      const fixedOn = fix(on);
      initVisualRegressionPlugin(fixedOn, config);
      // tutaj zarejestruj pozostałe pluginy, używając fixedOn
    },
  },
});

Jak zintegrować plugin z mochawesome lub innymi reporterami, które osadzają zrzuty ekranu?

Plugin przechowuje obrazy baseline poza domyślnym katalogiem cypress/screenshots/. Niektóre reportery (np. cypress-mochawesome-reporter) oczekują, że zrzuty ekranu będą właśnie w tym folderze.

Użyj hooka taska processImgPath, żeby kontrolować, gdzie trafia plik zrzutu ekranu. Nadpisz go w setupNodeEvents po wywołaniu initPlugin:

setupNodeEvents(on, config) {
  initVisualRegressionPlugin(on, config);

  // nadpisz przetwarzanie ścieżki, żeby zrzuty ekranu pozostały dostępne dla reportera
  on('task', {
    'cp-visual-regression-diff-processImgPath': ({ path }: { path: string }) => {
      // zwróć nową ścieżkę albo tę samą, żeby zachować domyślne zachowanie
      return path;
    },
  });
}

Jak oblać test przy każdej różnicy wizualnej, nawet najmniejszej?

Ustaw maxDiffThreshold na 0:

cy.matchImage({ maxDiffThreshold: 0 });

Albo globalnie przez zmienną expose:

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionMaxDiffThreshold=0"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionMaxDiffThreshold=0"

Dlaczego aktualny zrzut ekranu jest usuwany po udanym porównaniu obrazów?

Gdy obrazy się zgadzają, plik .actual.png jest tymczasowy i po porównaniu jest sprzątany. Zostaje tylko obraz baseline (bez sufiksu). Dzięki temu nie trzymamy zbędnych plików.

Gdy obrazy się nie zgadzają, plik .actual.png zostaje i niesie te same metadane pluginu co obraz baseline. Możesz go bezpiecznie przemianować lub skopiować w miejsce baseline'u (np. w jobie CI, który otwiera pull requesta ze zaktualizowanymi baseline'ami) - plugin potraktuje go jako aktualny baseline i nie nadpisze go przy następnym uruchomieniu.

Jeśli potrzebujesz, żeby aktualne zrzuty ekranu pozostały dostępne dla reportera (np. cypress-mochawesome-reporter), użyj hooka taska processImgPath, żeby kontrolować, gdzie są zapisywane. Zobacz Jak zintegrować plugin z mochawesome lub innymi reporterami, które osadzają zrzuty ekranu?

Jak usunąć spacje z nazw plików zrzutów ekranu?

Nazwy plików zrzutów ekranu pochodzą od tytułów testów Cypress, które mogą zawierać spacje. Żeby użyć nazwy pliku bez spacji, przekaż własną opcję title do każdego wywołania matchImage:

cy.matchImage({ title: 'my-screenshot-without-spaces' });

Żeby zastosować to globalnie dla wszystkich wywołań matchImage, nadpisz komendę w pliku support (cypress/support/commands.ts):

Cypress.Commands.overwrite('matchImage', (originalFn, subject, options = {}) =>
  originalFn(subject, {
    title: Cypress.currentTest.titlePath.join(' ').replace(/\s+/g, '-'),
    ...options,
  }),
);

Jak dodać nazwę przeglądarki do nazw plików obrazów (testy cross-browser)?

Użyj tokenu {browser} w imagesPath; rozwija się on do nazwy przeglądarki, która wyrenderowała zrzut ekranu (chrome, firefox, electron, ...):

cy.matchImage({ imagesPath: '{spec_path}/__image_snapshots__/{browser}' });

Albo globalnie:

# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionImagesPath={spec_path}/__image_snapshots__/{browser}"
# Cypress <15.10 (przestarzałe od 15.10, usunięte w 16)
npx cypress run --env "pluginVisualRegressionImagesPath={spec_path}/__image_snapshots__/{browser}"

W ten sposób powstają osobne katalogi obrazów dla każdej przeglądarki (__image_snapshots__/chrome/, __image_snapshots__/firefox/). W sekcji Osobne baseline'y dla każdej platformy znajdziesz {platform} i {os}, które dzielą baseline'y także według systemu operacyjnego, informację o tym, które katalogi commitować, oraz wyjaśnienie, dlaczego pluginVisualRegressionCleanupUnusedImages musi pozostać wyłączone na maszynie, która ma katalogi więcej niż jednej przeglądarki (usunęłoby baseline'y pozostałych przeglądarek).

Jak wygenerować raport HTML z obrazami baseline, diff i aktualnym?

Plugin ma wbudowaną nakładkę porównania wizualnego w UI Cypress: kliknięcie "See comparison" przy nieudanym teście pokazuje obok siebie obraz baseline, diff i aktualny.

Na CI albo do raportów HTML, którymi chcesz się dzielić, zintegruj plugin z reporterem takim jak cypress-mochawesome-reporter. Szczegóły konfiguracji znajdziesz w Jak zintegrować plugin z mochawesome lub innymi reporterami, które osadzają zrzuty ekranu?.

Tłumaczenie kuleje? Pomóż nam i popraw je samodzielnie.