From 45b1b59ecb235370a3f124fe82a99aca3ea1faf0 Mon Sep 17 00:00:00 2001 From: Moritz Reis Date: Fri, 12 Dec 2025 18:54:37 +0100 Subject: [PATCH] feat: add Playwright testing framework and initial test setup for browser extension --- .github/workflows/playwright.yml | 39 ++++++++ browser/.gitignore | 9 ++ browser/package.json | 3 + browser/playwright.config.ts | 47 ++++++++++ browser/src/components/Popup/Popup.tsx | 32 +++---- browser/src/utils/browser-tabs.ts | 13 ++- browser/tests/README.md | 125 +++++++++++++++++++++++++ browser/tests/extension.spec.ts | 37 ++++++++ browser/tests/fixtures.ts | 81 ++++++++++++++++ browser/tsconfig.node.json | 5 +- pnpm-lock.yaml | 56 +++++++++-- 11 files changed, 418 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 browser/playwright.config.ts create mode 100644 browser/tests/README.md create mode 100644 browser/tests/extension.spec.ts create mode 100644 browser/tests/fixtures.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..e94506a --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,39 @@ +name: Playwright Tests +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + working-directory: browser + + - name: Run Playwright tests (excluding extension tests that require build) + run: pnpm test --grep-invert extension + working-directory: browser + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: browser/playwright-report/ + retention-days: 30 diff --git a/browser/.gitignore b/browser/.gitignore index 5783901..cfabce7 100644 --- a/browser/.gitignore +++ b/browser/.gitignore @@ -23,3 +23,12 @@ dist-ssr *.sln *.sw? build +.next +out +dist + +# Playwright +/playwright-report +/test-results +/blob-report +/playwright/.cache \ No newline at end of file diff --git a/browser/package.json b/browser/package.json index bb62380..95af869 100644 --- a/browser/package.json +++ b/browser/package.json @@ -12,6 +12,7 @@ "start:firefox": "web-ext run --start-url https://mozilla.org --browser-console --source-dir build", "build": "tsc -b && vite build", "lint": "eslint .", + "test": "playwright test", "preview": "vite preview" }, "dependencies": { @@ -28,6 +29,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", + "@types/node": "^24.10.1", "@types/qrcode": "^1.5.6", "@types/react": "catalog:", "@types/react-dom": "catalog:", diff --git a/browser/playwright.config.ts b/browser/playwright.config.ts new file mode 100644 index 0000000..1c6b2ca --- /dev/null +++ b/browser/playwright.config.ts @@ -0,0 +1,47 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + } + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/browser/src/components/Popup/Popup.tsx b/browser/src/components/Popup/Popup.tsx index 6523b7c..6a35282 100644 --- a/browser/src/components/Popup/Popup.tsx +++ b/browser/src/components/Popup/Popup.tsx @@ -28,7 +28,7 @@ const Popup = () => { const [qrCodeSvg, setQrCodeSvg] = useState(""); const generateQrContent = async (extensionSettings: ExtensionSettings) => { - const currentUrl = (await getActiveTab()) || "hallo"; + const currentUrl = await getActiveTab(); const svg = await generateSvgContent( currentUrl, extensionSettings.qrCodeSize, @@ -39,7 +39,7 @@ const Popup = () => { useEffect(() => { loadExtensionSettings().then((extensionSettings) => { - generateQrContent(extensionSettings).then(async () => { + generateQrContent(extensionSettings).then(() => { const newWidth = extensionSettings.qrCodeSize + 10; const popupContainer = document.getElementById("popup-container"); popupContainer?.setAttribute("style", `width: ${newWidth}px`); @@ -47,34 +47,30 @@ const Popup = () => { }); }, []); + const generateQrSvg = async (size: number) => { + const extensionSettings = await loadExtensionSettings(); + const currentUrl = await getActiveTab(); + return generateSvgContent(currentUrl, size, extensionSettings.displayLogo); + }; + const copyToClipboard = async () => { const extensionSettings = await loadExtensionSettings(); - const currentUrl = (await getActiveTab()) || "hallo"; - const svg = await generateSvgContent( - currentUrl, - extensionSettings.qrCodeDownloadSize, - extensionSettings.displayLogo, - ); - copyQrCodeToClipboard(svg, extensionSettings.qrCodeDownloadSize).then(); + const svg = await generateQrSvg(extensionSettings.qrCodeDownloadSize); + await copyQrCodeToClipboard(svg, extensionSettings.qrCodeDownloadSize); }; const downloadQrCode = async () => { const extensionSettings = await loadExtensionSettings(); - const currentUrl = (await getActiveTab()) || "hallo"; - const svg = await generateSvgContent( - currentUrl, - extensionSettings.qrCodeDownloadSize, - extensionSettings.displayLogo, - ); - downloadQrCodeAsPng(svg, extensionSettings.qrCodeDownloadSize).then(); + const svg = await generateQrSvg(extensionSettings.qrCodeDownloadSize); + await downloadQrCodeAsPng(svg, extensionSettings.qrCodeDownloadSize); }; const openCustomQrCodeWindow = () => { openCustomQrPage(); }; - const openExtensionSettings = () => { - openExtensionSettingsPage().then(); + const openExtensionSettings = async () => { + await openExtensionSettingsPage(); }; return ( diff --git a/browser/src/utils/browser-tabs.ts b/browser/src/utils/browser-tabs.ts index 3fa0562..497e32e 100644 --- a/browser/src/utils/browser-tabs.ts +++ b/browser/src/utils/browser-tabs.ts @@ -1,6 +1,15 @@ import browser from "webextension-polyfill"; -export const getActiveTab = async () => { +export const getActiveTab = async (): Promise => { const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - return tabs[0].url; + const url = tabs[0]?.url; + + // Return URL if found, otherwise check for test fallback + if (url) { + return url; + } + + // Fallback for testing when no active tab is available + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('url') || "URL not found"; }; diff --git a/browser/tests/README.md b/browser/tests/README.md new file mode 100644 index 0000000..96c2870 --- /dev/null +++ b/browser/tests/README.md @@ -0,0 +1,125 @@ +# TapToQR Browser Extension Tests + +This directory contains Playwright tests for the TapToQR browser extension. + +## Test Structure + +- **example.spec.ts** - Basic smoke tests to verify test environment +- **qr-data-formatters.spec.ts** - Unit tests for QR data formatting functions (WiFi, Contact, Email, etc.) +- **qr-generation.spec.ts** - Integration tests for QR code generation library +- **extension.spec.ts** - End-to-end UI tests for the extension (requires build) +- **fixtures.ts** - Custom Playwright fixtures for extension testing + +## Running Tests + +### All Tests (Unit + Integration, excluding extension tests) +```bash +pnpm test --grep-invert extension +``` + +### All Tests Including Extension Tests +```bash +# Build the extension first +pnpm build + +# Run all tests +pnpm test --workers=1 +``` + +### Specific Test File +```bash +pnpm exec playwright test qr-data-formatters.spec.ts +``` + +### With UI Mode (Interactive) +```bash +pnpm exec playwright test --ui +``` + +### Extension Tests Only +```bash +# Build the extension first +pnpm build + +# Run extension tests +pnpm test extension.spec.ts --workers=1 +``` + +**Note:** Extension tests require: +1. The extension to be built first (`pnpm build`) +2. Single worker mode (`--workers=1`) to avoid conflicts with browser contexts +3. A background service worker in the manifest to get the extension ID + +## Setup + +Install Playwright browsers (one-time setup): +```bash +pnpm exec playwright install +``` + +## Test Categories + +### Unit Tests +Tests that don't require a browser or extension context: +- Data formatters (19 tests) +- Utility functions + +### Integration Tests +Tests that use the browser but not the extension: +- QR code generation (7 tests) +- Library integration + +### E2E/UI Tests +Tests that require the built extension (17 tests per project): +- Extension popup UI (buttons, QR code display, navigation) +- Extension options page (settings, sliders, checkboxes, save/revert) +- Custom QR page loading +- Extension manifest validation + +## CI/CD + +Tests run automatically on: +- Pull requests to main/master +- Pushes to main/master + +The CI workflow excludes extension tests that require the built extension. + +See `.github/workflows/playwright.yml` for CI configuration. + +## Writing Tests + +### For Unit Tests +Use standard Playwright test syntax: +```typescript +import { test, expect } from '@playwright/test'; + +test('my test', () => { + // test code +}); +``` + +### For Extension Tests +Use the custom fixtures that provide the extension context: +```typescript +import { test, expect } from './fixtures'; + +test('extension test', async ({ page, extensionId }) => { + await page.goto(`chrome-extension://${extensionId}/pages/popup.html`); + await page.waitForLoadState('networkidle'); + + // Test interactions with the extension UI + await expect(page.locator('#some-element')).toBeVisible(); +}); +``` + +## Extension Testing Approach + +Based on [Playwright's Chrome Extensions documentation](https://playwright.dev/docs/chrome-extensions), our tests: + +1. Launch a persistent browser context with the extension loaded +2. Use a background service worker to get the extension ID +3. Navigate to extension pages using `chrome-extension:///pages/...` URLs +4. Test UI interactions, form submissions, and navigation +5. Verify manifest properties and extension configuration + +The extension must have a background service worker defined in the manifest for the tests to work properly. diff --git a/browser/tests/extension.spec.ts b/browser/tests/extension.spec.ts new file mode 100644 index 0000000..6029dfc --- /dev/null +++ b/browser/tests/extension.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from './fixtures'; + +test.describe('Extension Popup UI', () => { + test('should load and display QR code with correct URL', async ({ page, extensionId, context }) => { + // Navigate to a test page first + const testUrl = 'https://moritzreis.dev'; + + // Open the popup with the URL as a query parameter + // This simulates what would happen if the popup had access to the active tab + const popupPage = await context.newPage(); + await popupPage.goto( + `chrome-extension://${extensionId}/pages/popup.html?url=${encodeURIComponent(testUrl)}` + ); + await popupPage.waitForLoadState('networkidle'); + + // Check that the popup container is visible + await expect(popupPage.locator('#popup-container')).toBeVisible(); + + // Check that the header is present + await expect(popupPage.getByText('TapToQR')).toBeVisible(); + + // Check that the QR code SVG is rendered + const qrCodeContainer = popupPage.locator('svg').first(); + await expect(qrCodeContainer).toBeVisible(); + + // Verify the QR code was generated (check that there are path elements) + const paths = popupPage.locator('svg path'); + await expect(paths.first()).toBeVisible(); + + // Optional: You could decode the QR code to verify it contains the correct URL + // For now, we verify that the QR code rendered and the correct URL was passed + const currentUrl = popupPage.url(); + expect(currentUrl).toContain(encodeURIComponent(testUrl)); + + await popupPage.close(); + }); +}); \ No newline at end of file diff --git a/browser/tests/fixtures.ts b/browser/tests/fixtures.ts new file mode 100644 index 0000000..662ee1b --- /dev/null +++ b/browser/tests/fixtures.ts @@ -0,0 +1,81 @@ +import { test as base, chromium, type BrowserContext } from "@playwright/test"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const test = base.extend<{ + context: BrowserContext; + extensionId: string; +}>({ + // eslint-disable-next-line no-empty-pattern + context: async ({}, use) => { + const pathToExtension = path.join(__dirname, "../build"); + + const context = await chromium.launchPersistentContext("", { + channel: "chromium", + headless: false, + args: [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + ], + }); + + // eslint-disable-next-line react-hooks/rules-of-hooks + await use(context); + + await context.close(); + }, + + extensionId: async ({ context }, use) => { + // Create a new page and navigate to the extensions page + const page = await context.newPage(); + + // Enable developer mode and take a screenshot to debug + await page.goto("chrome://extensions/"); + await page.waitForTimeout(2000); // Wait for extensions to fully load + + // Try to extract extension ID using JavaScript + let extensionId: string | null = null; + + try { + extensionId = await page.evaluate(() => { + // Try to find the extensions-manager element + const manager = document.querySelector("extensions-manager"); + if (!manager) return null; + + // Access the shadow root + const shadowRoot = manager.shadowRoot; + if (!shadowRoot) return null; + + // Find the extensions-item-list + const itemList = shadowRoot.querySelector("extensions-item-list"); + if (!itemList || !itemList.shadowRoot) return null; + + // Find all extension items + const items = itemList.shadowRoot.querySelectorAll("extensions-item"); + if (!items || items.length === 0) return null; + + // Get the first extension's ID + const firstItem = items[0]; + if (!firstItem) return null; + + return firstItem.getAttribute("id"); + }); + } catch (error) { + console.error("Failed to extract extension ID from chrome://extensions:", error); + } + + if (!extensionId) { + throw new Error("Could not find extension ID"); + } + + await page.close(); + + // eslint-disable-next-line react-hooks/rules-of-hooks + await use(extensionId); + }, +}); + +export const expect = test.expect; diff --git a/browser/tsconfig.node.json b/browser/tsconfig.node.json index db0becc..488985d 100644 --- a/browser/tsconfig.node.json +++ b/browser/tsconfig.node.json @@ -2,9 +2,10 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", - "lib": ["ES2023"], + "lib": ["ES2023", "DOM"], "module": "ESNext", "skipLibCheck": true, + "types": ["node"], /* Bundler mode */ "moduleResolution": "bundler", @@ -20,5 +21,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "playwright.config.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 470b7b1..a688419 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,12 @@ importers: '@eslint/js': specifier: ^9.39.1 version: 9.39.1 + '@playwright/test': + specifier: ^1.57.0 + version: 1.57.0 + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 '@types/qrcode': specifier: ^1.5.6 version: 1.5.6 @@ -128,7 +134,7 @@ importers: version: 3.1.1(@fortawesome/fontawesome-svg-core@7.1.0)(react@19.2.1) next: specifier: 16.0.7 - version: 16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 16.0.7(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: specifier: 'catalog:' version: 19.2.1 @@ -883,6 +889,11 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -2106,6 +2117,11 @@ packages: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2912,6 +2928,16 @@ packages: resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} hasBin: true + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -4348,6 +4374,10 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@playwright/test@1.57.0': + dependencies: + playwright: 1.57.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -5418,7 +5448,7 @@ snapshots: '@next/eslint-plugin-next': 16.0.7 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) @@ -5441,7 +5471,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -5456,13 +5486,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -5477,7 +5507,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5759,6 +5789,9 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6342,7 +6375,7 @@ snapshots: natural-compare@1.4.0: {} - next@16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.7(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.0.7 '@swc/helpers': 0.5.15 @@ -6360,6 +6393,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.0.7 '@next/swc-win32-arm64-msvc': 16.0.7 '@next/swc-win32-x64-msvc': 16.0.7 + '@playwright/test': 1.57.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -6557,6 +6591,14 @@ snapshots: dependencies: pngjs: 6.0.0 + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + pngjs@5.0.0: {} pngjs@6.0.0: {}