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>
This commit is contained in:
senke 2026-02-07 20:01:30 +01:00
parent 995063383f
commit be7d7b02cc
13 changed files with 451 additions and 36 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View 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,
});
});
});
});

View file

@ -15,6 +15,9 @@
"test:e2e:msw": "cross-env VITE_USE_MSW=1 playwright test", "test:e2e:msw": "cross-env VITE_USE_MSW=1 playwright test",
"test:e2e:mocks": "playwright test --config=playwright.config.mocks.ts", "test:e2e:mocks": "playwright test --config=playwright.config.mocks.ts",
"test:e2e:mocks:ui": "playwright test --config=playwright.config.mocks.ts --ui", "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": "eslint . --ext ts,tsx",
"lint:fix": "eslint . --ext ts,tsx --fix", "lint:fix": "eslint . --ext ts,tsx --fix",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
@ -81,7 +84,7 @@
"devDependencies": { "devDependencies": {
"@lhci/cli": "^0.12.0", "@lhci/cli": "^0.12.0",
"@openapitools/openapi-generator-cli": "^2.27.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-a11y": "^8.6.15",
"@storybook/addon-essentials": "^8.6.15", "@storybook/addon-essentials": "^8.6.15",
"@storybook/addon-interactions": "^8.6.15", "@storybook/addon-interactions": "^8.6.15",
@ -117,7 +120,9 @@
"msw-storybook-addon": "^2.0.6", "msw-storybook-addon": "^2.0.6",
"newman": "^6.1.0", "newman": "^6.1.0",
"pa11y-ci": "^3.0.1", "pa11y-ci": "^3.0.1",
"pixelmatch": "^5.3.0",
"playwright": "^1.58.1", "playwright": "^1.58.1",
"pngjs": "^7.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"storybook": "^8.6.15", "storybook": "^8.6.15",
"storybook-dark-mode": "^4.0.2", "storybook-dark-mode": "^4.0.2",

View 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,
},
});

View 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);
}

View 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` sil existe ; sinon certains tests (ex. sidebar) sont skippés. Pour créer ou mettre à jour les baselines authentifiés, exécuter dabord 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 lURL.
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 denvironnement **`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 lUI, 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 dun run à lautre.
- **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 lanalyse des diffs.

71
package-lock.json generated
View file

@ -64,7 +64,7 @@
"devDependencies": { "devDependencies": {
"@lhci/cli": "^0.12.0", "@lhci/cli": "^0.12.0",
"@openapitools/openapi-generator-cli": "^2.27.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-a11y": "^8.6.15",
"@storybook/addon-essentials": "^8.6.15", "@storybook/addon-essentials": "^8.6.15",
"@storybook/addon-interactions": "^8.6.15", "@storybook/addon-interactions": "^8.6.15",
@ -100,7 +100,9 @@
"msw-storybook-addon": "^2.0.6", "msw-storybook-addon": "^2.0.6",
"newman": "^6.1.0", "newman": "^6.1.0",
"pa11y-ci": "^3.0.1", "pa11y-ci": "^3.0.1",
"pixelmatch": "^5.3.0",
"playwright": "^1.58.1", "playwright": "^1.58.1",
"pngjs": "^7.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"storybook": "^8.6.15", "storybook": "^8.6.15",
"storybook-dark-mode": "^4.0.2", "storybook-dark-mode": "^4.0.2",
@ -309,36 +311,37 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"apps/web/node_modules/playwright": { "apps/web/node_modules/pixelmatch": {
"version": "1.58.1", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "ISC",
"dependencies": { "dependencies": {
"playwright-core": "1.58.1" "pngjs": "^6.0.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "pixelmatch": "bin/pixelmatch"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
} }
}, },
"apps/web/node_modules/playwright-core": { "apps/web/node_modules/pixelmatch/node_modules/pngjs": {
"version": "1.58.1", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "MIT",
"bin": {
"playwright-core": "cli.js"
},
"engines": { "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": { "apps/web/node_modules/react-docgen": {
@ -2603,13 +2606,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.57.0", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.57.0" "playwright": "1.58.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -15864,13 +15867,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.57.0", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.57.0" "playwright-core": "1.58.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -15883,9 +15886,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.57.0", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {