feat(e2e): Playwright + pixelmatch stack for pixel-perfect visual regression
- playwright.config.visual.ts: dedicated config, viewport 1280x720, Chromium only, snapshots in e2e/tests/visual/__snapshots__ - e2e/tests/visual/visual-regression.spec.ts: login, register, dashboard (full/header/sidebar), player bar, playlists, 404, mobile/tablet viewports; dark theme + reduceMotion - scripts/visual-diff.js: optional pixelmatch script to generate diff image from two PNGs - docs/VISUAL_TESTING_STRATEGY.md: strategy, commands, CI, workflow - npm scripts: test:visual, test:visual:update, test:visual:report - deps: pixelmatch, pngjs; @playwright/test aligned to 1.58.1 - baseline snapshots added for login, dashboard, playlists, 404, viewports Co-authored-by: Cursor <cursoragent@cursor.com>
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 235 KiB |
167
apps/web/e2e/tests/visual/visual-regression.spec.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_CONFIG } from '../../utils/test-helpers';
|
||||
|
||||
/** Pixel-perfect visual regression: strict by default. Relax in CI if needed via VISUAL_MAX_DIFF_PIXELS. */
|
||||
const MAX_DIFF_PIXELS = process.env.VISUAL_MAX_DIFF_PIXELS ? parseInt(process.env.VISUAL_MAX_DIFF_PIXELS, 10) : 0;
|
||||
const ANIMATION_SETTLE_MS = 800;
|
||||
|
||||
async function ensureDarkTheme(page: import('@playwright/test').Page) {
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
test.describe('Visual regression (pixel-perfect)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
});
|
||||
|
||||
test.describe('Auth pages (no storage)', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('login page', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('form', { timeout: 10000 });
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(page).toHaveScreenshot('login-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
|
||||
test('register page', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/register`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('form, [role="form"], input[type="email"]', { timeout: 15000 }).catch(() => {});
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(page).toHaveScreenshot('register-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('App shell (authenticated)', () => {
|
||||
test('dashboard full page', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('main, [role="main"]', { timeout: 15000 });
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(page).toHaveScreenshot('dashboard-full.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
|
||||
test('dashboard header only', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const header = page.locator('header').first();
|
||||
await header.waitFor({ timeout: 10000 });
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(header).toHaveScreenshot('dashboard-header.png', {
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
|
||||
test('dashboard sidebar only', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const sidebar = page.locator('aside').first();
|
||||
const visible = await sidebar.waitFor({ state: 'visible', timeout: 12000 }).then(() => true).catch(() => false);
|
||||
if (!visible) {
|
||||
test.skip(true, 'Sidebar not visible (e.g. not authenticated or mobile layout)');
|
||||
return;
|
||||
}
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(sidebar).toHaveScreenshot('dashboard-sidebar.png', {
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
|
||||
test('global player bar', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
const playerBar = page.locator('div.fixed.bottom-0.left-0.right-0').first();
|
||||
await playerBar.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
|
||||
if ((await playerBar.count()) === 0) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
await expect(playerBar).toHaveScreenshot('player-bar.png', {
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Key routes', () => {
|
||||
test('playlists page', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/playlists`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('main, [role="main"]', { timeout: 10000 }).catch(() => {});
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(page).toHaveScreenshot('playlists-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
|
||||
test('404 page', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/non-existent-route-404`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await ensureDarkTheme(page);
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
|
||||
await expect(page).toHaveScreenshot('404-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Viewports', () => {
|
||||
test('dashboard mobile 375x667', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
await ensureDarkTheme(page);
|
||||
|
||||
await expect(page).toHaveScreenshot('dashboard-mobile.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
|
||||
test('dashboard tablet 768x1024', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(ANIMATION_SETTLE_MS);
|
||||
await ensureDarkTheme(page);
|
||||
|
||||
await expect(page).toHaveScreenshot('dashboard-tablet.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: MAX_DIFF_PIXELS,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -15,6 +15,9 @@
|
|||
"test:e2e:msw": "cross-env VITE_USE_MSW=1 playwright test",
|
||||
"test:e2e:mocks": "playwright test --config=playwright.config.mocks.ts",
|
||||
"test:e2e:mocks:ui": "playwright test --config=playwright.config.mocks.ts --ui",
|
||||
"test:visual": "playwright test --config=playwright.config.visual.ts",
|
||||
"test:visual:update": "playwright test --config=playwright.config.visual.ts --update-snapshots",
|
||||
"test:visual:report": "playwright show-report e2e/playwright-report-visual",
|
||||
"lint": "eslint . --ext ts,tsx",
|
||||
"lint:fix": "eslint . --ext ts,tsx --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
@ -81,7 +84,7 @@
|
|||
"devDependencies": {
|
||||
"@lhci/cli": "^0.12.0",
|
||||
"@openapitools/openapi-generator-cli": "^2.27.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
|
|
@ -117,7 +120,9 @@
|
|||
"msw-storybook-addon": "^2.0.6",
|
||||
"newman": "^6.1.0",
|
||||
"pa11y-ci": "^3.0.1",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"playwright": "^1.58.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"storybook": "^8.6.15",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
|
|
|
|||
64
apps/web/playwright.config.visual.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Playwright config for pixel-perfect visual regression tests.
|
||||
*
|
||||
* - Fixed viewport and single browser (Chromium) for reproducible screenshots.
|
||||
* - Snapshots stored in e2e/snapshots/ for easy review and CI artifact.
|
||||
* - Optional: run without global auth for login/register snapshots.
|
||||
*
|
||||
* Run:
|
||||
* npx playwright test --config=playwright.config.visual.ts
|
||||
* Update baselines:
|
||||
* npx playwright test --config=playwright.config.visual.ts --update-snapshots
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e/tests/visual',
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: 1,
|
||||
timeout: 30000,
|
||||
|
||||
outputDir: 'e2e/test-results-visual',
|
||||
snapshotPathTemplate: '{testDir}/__snapshots__/{arg}-{projectName}{ext}',
|
||||
|
||||
reporter: [
|
||||
['html', { outputFolder: 'e2e/playwright-report-visual', open: 'never' }],
|
||||
['list'],
|
||||
],
|
||||
|
||||
use: {
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || process.env.VITE_FRONTEND_URL || 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'off',
|
||||
// Fixed viewport for pixel-perfect comparison (no cross-resolution variance)
|
||||
viewport: { width: 1280, height: 720 },
|
||||
// Storage state: set per-test for login (no auth) vs dashboard (auth)
|
||||
storageState: process.env.VISUAL_AUTH_STATE || 'e2e/.auth/user.json',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium-desktop',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: { width: 1280, height: 720 },
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: false,
|
||||
hasTouch: false,
|
||||
locale: 'en-US',
|
||||
timezoneId: 'Europe/Paris',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: true,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
63
apps/web/scripts/visual-diff.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate a pixel diff image between two PNGs using pixelmatch.
|
||||
* Usage: node scripts/visual-diff.js <expected.png> <actual.png> [diff-output.png] [threshold]
|
||||
*
|
||||
* When Playwright fails a visual test it writes:
|
||||
* - expected: test-results/.../expected-*.png
|
||||
* - actual: test-results/.../actual-*.png
|
||||
* You can run this script to get a single diff image (pixels that differ in red).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const expectedPath = process.argv[2];
|
||||
const actualPath = process.argv[3];
|
||||
const diffOutputPath = process.argv[4] || path.join(path.dirname(actualPath || ''), 'diff.png');
|
||||
const threshold = parseFloat(process.argv[5] || '0.1', 10);
|
||||
|
||||
if (!expectedPath || !actualPath) {
|
||||
console.error('Usage: node scripts/visual-diff.js <expected.png> <actual.png> [diff-output.png] [threshold]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function loadPng(filePath) {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return PNG.sync.read(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const imgExpected = loadPng(expectedPath);
|
||||
const imgActual = loadPng(actualPath);
|
||||
|
||||
if (imgExpected.width !== imgActual.width || imgExpected.height !== imgActual.height) {
|
||||
console.error('Image dimensions differ:', imgExpected.width, 'x', imgExpected.height, 'vs', imgActual.width, 'x', imgActual.height);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { width, height } = imgExpected;
|
||||
const diff = new PNG({ width, height });
|
||||
const numDiffPixels = pixelmatch(
|
||||
imgExpected.data,
|
||||
imgActual.data,
|
||||
diff.data,
|
||||
width,
|
||||
height,
|
||||
{ threshold }
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(diffOutputPath), { recursive: true });
|
||||
fs.writeFileSync(diffOutputPath, PNG.sync.write(diff));
|
||||
console.log(`Diff pixels: ${numDiffPixels}`);
|
||||
console.log(`Diff image written: ${diffOutputPath}`);
|
||||
process.exit(numDiffPixels > 0 ? 1 : 0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
113
docs/VISUAL_TESTING_STRATEGY.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Stratégie de tests visuels (Playwright + pixelmatch)
|
||||
|
||||
Objectif : **vue fiable du frontend** via des screenshots automatiques et une comparaison **pixel-perfect** pour éviter les régressions visuelles.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack
|
||||
|
||||
| Outil | Rôle |
|
||||
|-------|------|
|
||||
| **Playwright** | Lancement du navigateur, navigation, capture d’écran, comparaison avec les baselines. |
|
||||
| **toHaveScreenshot()** | Comparaison pixel à pixel (Playwright utilise en interne un algorithme type pixelmatch). |
|
||||
| **pixelmatch + pngjs** | Script optionnel `scripts/visual-diff.js` pour générer une image de diff à partir de deux PNG (expected/actual). |
|
||||
|
||||
Les baselines (golden screenshots) sont versionnées dans le dépôt. En CI, on compare les nouvelles captures aux baselines ; en local, on peut mettre à jour les baselines après un changement volontaire.
|
||||
|
||||
---
|
||||
|
||||
## 2. Config dédiée : `playwright.config.visual.ts`
|
||||
|
||||
- **Répertoire des tests** : `e2e/tests/visual/`
|
||||
- **Snapshots** : `e2e/tests/visual/__snapshots__/` (un fichier par test, nommé `{arg}-{projectName}{ext}`).
|
||||
- **Viewport fixe** : 1280×720 (Chromium uniquement) pour des captures reproductibles.
|
||||
- **Réduction des animations** : `reduceMotion: 'reduce'` pour limiter la flou du à des animations.
|
||||
- **Un worker** : exécution séquentielle pour éviter les variations de charge.
|
||||
- **Auth** : les tests login/register utilisent un contexte sans cookie. Les tests “app” (dashboard, sidebar, player) utilisent `e2e/.auth/user.json` s’il existe ; sinon certains tests (ex. sidebar) sont skippés. Pour créer ou mettre à jour les baselines authentifiés, exécuter d’abord les tests E2E normaux (avec global setup) une fois : `npx playwright test --config=playwright.config.ts` (cela crée `e2e/.auth/user.json`), puis `npm run test:visual:update`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Suite de tests : `e2e/tests/visual/visual-regression.spec.ts`
|
||||
|
||||
- **Auth (sans cookie)** : login, register — full page, thème dark forcé.
|
||||
- **App (avec auth)** : dashboard full page, header, sidebar, barre de lecture — thème dark forcé.
|
||||
- **Routes** : playlists, 404.
|
||||
- **Viewports** : mobile 375×667, tablette 768×1024.
|
||||
|
||||
Chaque test :
|
||||
|
||||
1. Navigue vers l’URL.
|
||||
2. Attend `networkidle` et les éléments principaux.
|
||||
3. Force le thème dark (`document.documentElement.classList.add('dark')`).
|
||||
4. Attend un délai de stabilisation (800 ms).
|
||||
5. Appelle `expect(...).toHaveScreenshot(...)` avec `maxDiffPixels` (et éventuellement `maxDiffPixelRatio`).
|
||||
|
||||
**Tolérance pixel-perfect** : par défaut `maxDiffPixels: 0`. En CI, si besoin (police, anti-aliasing), on peut relâcher via la variable d’environnement **`VISUAL_MAX_DIFF_PIXELS`** (ex. `50`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Commandes npm (apps/web)
|
||||
|
||||
```bash
|
||||
# Lancer les tests visuels (compare aux baselines)
|
||||
npm run test:visual
|
||||
|
||||
# Mettre à jour les baselines après un changement volontaire
|
||||
npm run test:visual:update
|
||||
|
||||
# Ouvrir le rapport HTML des derniers runs
|
||||
npm run test:visual:report
|
||||
```
|
||||
|
||||
En CI, exécuter `npm run test:visual` après le build. En cas d’échec, les artefacts contiennent les dossiers `test-results-visual` avec expected / actual / diff.
|
||||
|
||||
---
|
||||
|
||||
## 5. Workflow recommandé
|
||||
|
||||
1. **Développement** : modifier l’UI, lancer `npm run test:visual`. Si le changement est voulu, lancer `npm run test:visual:update` et committer les nouveaux fichiers dans `e2e/tests/visual/__snapshots__/`.
|
||||
2. **CI** : exécuter `npm run test:visual` (sans `--update-snapshots`). En cas d’échec, télécharger les artefacts (expected, actual, diff) pour analyser.
|
||||
3. **Optionnel** : après un échec, utiliser `node scripts/visual-diff.js <expected.png> <actual.png> [diff.png]` pour générer une image de diff avec pixelmatch (seuillage personnalisable).
|
||||
|
||||
---
|
||||
|
||||
## 6. Bonnes pratiques
|
||||
|
||||
- **Thème** : forcer `dark` dans les tests pour des baselines cohérentes.
|
||||
- **Viewport** : ne pas changer de taille dans un même projet “visual” sauf tests dédiés (mobile/tablette).
|
||||
- **Données** : utiliser des mocks ou un compte de test stable pour que le contenu (dashboard, playlists) ne change pas d’un run à l’autre.
|
||||
- **Animations** : `reduceMotion` + délai de stabilisation pour éviter les flous.
|
||||
- **Nommage** : noms de snapshots courts et explicites (ex. `login-page.png`, `dashboard-sidebar.png`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Fichiers clés
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---------|------|
|
||||
| `playwright.config.visual.ts` | Config viewport, snapshot path, projet Chromium. |
|
||||
| `e2e/tests/visual/visual-regression.spec.ts` | Tous les tests `toHaveScreenshot`. |
|
||||
| `e2e/tests/visual/__snapshots__/` | Baselines PNG (à committer). |
|
||||
| `scripts/visual-diff.js` | Script optionnel pixelmatch pour diff image. |
|
||||
| `docs/VISUAL_TESTING_STRATEGY.md` | Ce document. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Intégration CI (exemple)
|
||||
|
||||
```yaml
|
||||
# Exemple GitHub Actions
|
||||
- name: Run visual regression
|
||||
run: cd apps/web && npm run test:visual
|
||||
env:
|
||||
VISUAL_MAX_DIFF_PIXELS: "0" # ou "50" si tolérance acceptée
|
||||
|
||||
- name: Upload visual test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: visual-test-results
|
||||
path: apps/web/e2e/test-results-visual/
|
||||
```
|
||||
|
||||
Cette stratégie donne une **vue automatique et pixel-perfect du frontend** via Playwright et, en option, pixelmatch pour l’analyse des diffs.
|
||||
71
package-lock.json
generated
|
|
@ -64,7 +64,7 @@
|
|||
"devDependencies": {
|
||||
"@lhci/cli": "^0.12.0",
|
||||
"@openapitools/openapi-generator-cli": "^2.27.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
|
|
@ -100,7 +100,9 @@
|
|||
"msw-storybook-addon": "^2.0.6",
|
||||
"newman": "^6.1.0",
|
||||
"pa11y-ci": "^3.0.1",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"playwright": "^1.58.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"storybook": "^8.6.15",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
|
|
@ -309,36 +311,37 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"apps/web/node_modules/playwright": {
|
||||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz",
|
||||
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==",
|
||||
"apps/web/node_modules/pixelmatch": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
|
||||
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.1"
|
||||
"pngjs": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"apps/web/node_modules/playwright-core": {
|
||||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz",
|
||||
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==",
|
||||
"apps/web/node_modules/pixelmatch/node_modules/pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"apps/web/node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"apps/web/node_modules/react-docgen": {
|
||||
|
|
@ -2603,13 +2606,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.57.0"
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -15864,13 +15867,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.57.0"
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -15883,9 +15886,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
|
|||