diff --git a/apps/web/e2e/tests/visual/__snapshots__/404-page-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/404-page-chromium-desktop.png new file mode 100644 index 000000000..e3c21313c Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/404-page-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/__snapshots__/dashboard-full-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/dashboard-full-chromium-desktop.png new file mode 100644 index 000000000..5dd5bf168 Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/dashboard-full-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/__snapshots__/dashboard-header-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/dashboard-header-chromium-desktop.png new file mode 100644 index 000000000..470ff72d2 Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/dashboard-header-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/__snapshots__/dashboard-mobile-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/dashboard-mobile-chromium-desktop.png new file mode 100644 index 000000000..0d4526fb2 Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/dashboard-mobile-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/__snapshots__/dashboard-tablet-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/dashboard-tablet-chromium-desktop.png new file mode 100644 index 000000000..ab8ffdee7 Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/dashboard-tablet-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/__snapshots__/login-page-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/login-page-chromium-desktop.png new file mode 100644 index 000000000..5dd5bf168 Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/login-page-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/__snapshots__/playlists-page-chromium-desktop.png b/apps/web/e2e/tests/visual/__snapshots__/playlists-page-chromium-desktop.png new file mode 100644 index 000000000..5dd5bf168 Binary files /dev/null and b/apps/web/e2e/tests/visual/__snapshots__/playlists-page-chromium-desktop.png differ diff --git a/apps/web/e2e/tests/visual/visual-regression.spec.ts b/apps/web/e2e/tests/visual/visual-regression.spec.ts new file mode 100644 index 000000000..407bfdf1c --- /dev/null +++ b/apps/web/e2e/tests/visual/visual-regression.spec.ts @@ -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, + }); + }); + }); +}); diff --git a/apps/web/package.json b/apps/web/package.json index 2289f6b83..4040103ac 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", @@ -147,4 +152,4 @@ "public" ] } -} \ No newline at end of file +} diff --git a/apps/web/playwright.config.visual.ts b/apps/web/playwright.config.visual.ts new file mode 100644 index 000000000..40a55a9ae --- /dev/null +++ b/apps/web/playwright.config.visual.ts @@ -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, + }, +}); diff --git a/apps/web/scripts/visual-diff.js b/apps/web/scripts/visual-diff.js new file mode 100644 index 000000000..7f82e1c93 --- /dev/null +++ b/apps/web/scripts/visual-diff.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Generate a pixel diff image between two PNGs using pixelmatch. + * Usage: node scripts/visual-diff.js [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 [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); +} diff --git a/docs/VISUAL_TESTING_STRATEGY.md b/docs/VISUAL_TESTING_STRATEGY.md new file mode 100644 index 000000000..63e9fa9d7 --- /dev/null +++ b/docs/VISUAL_TESTING_STRATEGY.md @@ -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 [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. diff --git a/package-lock.json b/package-lock.json index 8657c05f0..e20014b0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": {