diff --git a/.gitignore b/.gitignore index d7d3689b..8ab78d26 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,19 @@ proxy/osac-proxy # AI workflow artifacts .artifacts/ /.claude/ + +# apps/playwright — manual browser verification against a live cluster, not +# an e2e test suite. Live session state and run output, never committed. +apps/playwright/.auth/ +apps/playwright/test-results/ +apps/playwright/playwright-report/ +apps/playwright/blob-report/ + +# apps/playwright — ad hoc specs are written here for manual verification and +# are never committed; only the harness itself is. +apps/playwright/scratch/ + +# apps/playwright — ad hoc specs sometimes call page.screenshot() with a bare +# filename, which lands in apps/playwright/ instead of a gitignored output dir. +apps/playwright/*.png +apps/playwright/*.jpg diff --git a/AGENTS.md b/AGENTS.md index 451127b9..aaf6d497 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,7 @@ Multi-stage build images: `nodejs-22-minimal:9.8`, `go-toolset:1.25`, `ubi-minim | `@osac/ui-components` | Shared components consumed at source (no build) — typed gRPC hooks live here | | `@osac/types` | Generated protobuf types and service descriptors — **never edit**, regenerate with `pnpm gen-types` | | `@osac/i18n` | Translation extraction — `locales/en/translation.json` is generated, not hand-edited | +| `@osac/playwright` | Playwright harness for manual verification against a **live deployed cluster** — not a CI suite, not persisted test coverage. See [Manual verification against a live cluster](#manual-verification-against-a-live-cluster) | ## Code Style @@ -179,9 +180,35 @@ export const getLabels = (t: TFunction) => ({ - `apps/app-frontend/vitest.config.ts` — single runner for app-frontend and ui-components tests (`include` spans both packages) - ESLint relaxes type safety rules for test files (no-unsafe-* off) - Testing libraries: @testing-library/react 16.x, @testing-library/jest-dom 6.x -- No E2E tests in this repo — E2E coverage lives in `osac-test-infra` +- No persisted E2E test suite in this repo, and no persisted UI-level E2E coverage anywhere: `osac-test-infra`'s CI-tracked E2E suites hit the fulfillment gRPC/REST API and Kubernetes CRs directly — no browser, nothing UI-related - CI runs lint, test, and container build; run `pnpm test` locally before submitting +### Manual verification against a live cluster + +`apps/playwright` (`@osac/playwright`) is a Playwright harness for manually confirming +that a change works end-to-end against a **live, already-deployed** cluster — +not a CI suite and not something that adds to this repo's persisted test count. See +[apps/playwright/README.md](apps/playwright/README.md). + +- **This is not a test suite — specs are throwaway**: write ad hoc specs under + `apps/playwright/scratch/`, a gitignored directory that only exists locally. + Nothing there is ever committed, but gitignore doesn't stop it from running — + `pnpm playwright` reruns every spec still in the directory, so delete a spec + once you're done with it. Only the harness itself (`playwright.config.ts`, + `auth.setup.ts`) plus one fixed exception, `src/smoke.spec.ts` (a persisted + harness self-check — loads the app, asserts the masthead renders — not a + feature test), is committed. Never write other specs under + `apps/playwright/src/`, and never treat a `scratch/` spec as regression + coverage. +- Iterating on a `scratch/` spec: `pnpm playwright:setup` logs in once, then + `pnpm playwright:run` re-runs just the `scratch/` specs without + re-authenticating — faster than `pnpm playwright`, which always re-logs in. +- Use this when asked to verify a UI change actually works in a browser against + a real deployment — requires `OSAC_UI_BASE_URL`, `OSAC_USERNAME`, `OSAC_PASSWORD` + (or `OSAC_UI_BASE_URL=http://localhost:5173` against a local `pnpm dev` + instance). There is no mock `fulfillment-service`, so this cannot run + hermetically or in CI. + ## Build **Frontend build** (apps/app-frontend): diff --git a/apps/playwright/README.md b/apps/playwright/README.md new file mode 100644 index 00000000..ead1c146 --- /dev/null +++ b/apps/playwright/README.md @@ -0,0 +1,175 @@ +# @osac/playwright + +Playwright browser tests for manually verifying `osac-ui` against a **live, +already-deployed** cluster — the running proxy, SPA, Keycloak realm, and +`fulfillment-service` backend all in one real environment. + +This is not a CI suite. There is no mock server for `fulfillment-service` +and no way to run this hermetically — it exists for AI agents to manually confirm a +change actually works end-to-end against a real deployment, the way you'd +otherwise do by hand in a browser. + +## Prerequisites + +- A reachable `osac-ui` deployment (any HTTPS URL that serves it). +- A Keycloak user on that deployment's realm to log in as. This package + doesn't provision one — use an existing test account, or create one on the + target environment first. +- Playwright's browser binaries installed once per machine (see Setup below). + +## Setup + +```bash +pnpm install +pnpm --filter @osac/playwright exec playwright install chromium +``` + +## Running + +Required environment variables: + +| Variable | Description | +| ------------------ | --------------------------------------------------------------------------------------------------- | +| `OSAC_UI_BASE_URL` | Full URL of the `osac-ui` deployment to test, e.g. `https://osac-ui-.` | +| `OSAC_USERNAME` | Username of a Keycloak test user on that deployment's realm | +| `OSAC_PASSWORD` | Password for that user | + +```bash +export OSAC_UI_BASE_URL=https://... OSAC_USERNAME=... +read -rs OSAC_PASSWORD && export OSAC_PASSWORD # avoids leaving the password in shell history +pnpm playwright +``` + +(`pnpm playwright` is a root-level shortcut for `pnpm --filter @osac/playwright +run playwright`.) None of these have defaults — each is required, so a missing +value fails immediately with a clear error instead of silently pointing at the +wrong environment. `pnpm playwright` logs in, runs the [smoke +test](#smoke-test), and runs every spec under `scratch/` — see [Writing a +test](#writing-a-test). + +For iterating on a `scratch/` spec, logging in on every run is slow and adds a +real Keycloak request each time. Log in once, then re-run just the spec as +many times as you need, reusing the saved session: + +```bash +pnpm playwright:setup # logs in once, writes .auth/user.json +pnpm playwright:run # runs scratch/ specs only, no login +pnpm playwright:run # ...repeat as many times as you're iterating +``` + +`pnpm playwright:run` never re-authenticates — if `.auth/user.json` is missing +or the session has expired, re-run `pnpm playwright:setup` first. + +Optional: + +| Variable | Description | +| -------------------------- | ----------------------------------------------------------------------------------------------------- | +| `IGNORE_HTTPS_ERRORS` | Set to `true` to trust self-signed/cluster-internal CA certs, common on dev and lab clusters. Off by default because this flow submits a real Keycloak password — it disables TLS verification for the whole browser context, not just `localhost`, so only enable it against clusters you trust. | + +Testing against a local `pnpm dev` instance instead of a remote deployment: +requires `pnpm dev` running in another terminal, and still needs +`OSAC_USERNAME`/`OSAC_PASSWORD`. + +```bash +export OSAC_UI_BASE_URL=http://localhost:5173 OSAC_USERNAME=... +read -rs OSAC_PASSWORD && export OSAC_PASSWORD +pnpm playwright +``` + +The backing `fulfillment-service`/Keycloak for a local dev server is often a +dev/lab cluster with a self-signed or cluster-internal CA cert — the real +login redirect goes there even though the UI itself is on `localhost`, so if +you hit a TLS error, explicitly opt in with `IGNORE_HTTPS_ERRORS=true` rather +than trusting it by default: + +```bash +export OSAC_UI_BASE_URL=http://localhost:5173 OSAC_USERNAME=... IGNORE_HTTPS_ERRORS=true +read -rs OSAC_PASSWORD && export OSAC_PASSWORD +pnpm playwright +``` + +## How authentication works + +`osac-ui` has no test-mode auth bypass — every login goes through a real +Keycloak realm via the app's normal OIDC flow. Rather than repeating that +flow (and Keycloak's own login page) for every test, this harness follows +Playwright's standard pattern for testing authenticated apps: + +1. A `setup` project (`src/auth.setup.ts`) opens the app, which auto-redirects + to Keycloak when unauthenticated, fills in `OSAC_USERNAME`/`OSAC_PASSWORD` + on Keycloak's real login form, and saves the resulting browser session + (cookies) to `.auth/user.json`. +2. Every real test (the `smoke` and `chromium` projects) declares a dependency + on `setup` and loads that saved session before the test body runs — so + tests start already logged in. + +`pnpm playwright` (and `pnpm playwright:setup`) always re-run `setup` first, +regenerating `.auth/user.json` fresh. `pnpm playwright:run` skips `setup` +entirely and reuses whatever session is already on disk — see +[Running](#running). + +`.auth/user.json` contains a live, real session (the actual `osac-access` +cookie) — it's gitignored and must never be committed or shared, and +`auth.setup.ts` restricts it to `0600` (and its directory to `0700`) so other +local accounts on a shared machine can't read it. + +If the target realm's login page uses a custom Keycloak theme, the field/ +button selectors in `auth.setup.ts` (written against Keycloak's default +theme) may need adjusting. + +## Smoke Test + +`src/smoke.spec.ts` is the one persisted, committed spec in this package — a +harness self-check, not a feature test. It logs in (via `setup`) and asserts +that the app actually loads and renders its masthead. Its job is to answer +"is the harness broken, or is my change broken?" before you spend time +writing a real `scratch/` spec: if the smoke test fails, the problem is +almost certainly auth, config, or environment — not the change you're trying +to verify. + +It runs automatically as part of `pnpm playwright` (and standalone via `pnpm +--filter @osac/playwright exec playwright test --project=smoke`). Unlike +`scratch/` specs, do not delete or repurpose it, and do not add more specs +here — this package still isn't a test suite; one fixed sanity check is the +exception, not a precedent. + +## Writing a test + +**This is not a test suite.** Beyond the one smoke test above, this package +provides the harness (auth + config) only. Test specs are throwaway, written +ad hoc for a specific manual-verification task (typically by an AI agent +working through a change) and run once, then discarded. + +Specs live under `scratch/` — a gitignored directory that only exists on your +machine, so nothing written there is ever committed or lands in a PR. Gitignore +only keeps it out of git, though — `pnpm playwright` re-runs every spec still +in the directory, so delete a spec once you're done with it. Do not write +specs under `src/` (that's reserved for the harness and the smoke test), and +do not treat anything under `scratch/` as regression coverage — this harness +has no CI and nothing under `scratch/` is ever committed. + +```bash +mkdir -p apps/playwright/scratch +cat > apps/playwright/scratch/check.spec.ts <<'EOF' +import { expect, test } from '@playwright/test'; + +test('loads the authenticated dashboard', async ({ page }) => { + await page.goto('/'); + + await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible(); +}); +EOF +``` + +It runs in the `chromium` project — already authenticated via `auth.setup.ts`, +no need to handle login in the test itself. Run it with: + +```bash +export OSAC_UI_BASE_URL=... OSAC_USERNAME=... +read -rs OSAC_PASSWORD && export OSAC_PASSWORD +pnpm playwright +``` + +`pnpm playwright` runs every spec under `scratch/` — delete the file when +you're done with it. Gitignore keeps it out of git either way, but it does +not stop Playwright from finding and re-running a stale spec next time. diff --git a/apps/playwright/package.json b/apps/playwright/package.json new file mode 100644 index 00000000..b1e2eda3 --- /dev/null +++ b/apps/playwright/package.json @@ -0,0 +1,17 @@ +{ + "name": "@osac/playwright", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "playwright": "playwright test", + "playwright:setup": "playwright test --project=setup", + "playwright:run": "playwright test --project=chromium --no-deps", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@playwright/test": "^1.62.0", + "@types/node": "^20.19.41", + "typescript": "~5.9.3" + } +} diff --git a/apps/playwright/playwright.config.ts b/apps/playwright/playwright.config.ts new file mode 100644 index 00000000..bb0b482f --- /dev/null +++ b/apps/playwright/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { AUTH_FILE } from './src/auth-file'; + +const baseURL = process.env.OSAC_UI_BASE_URL; +if (!baseURL) { + throw new Error('OSAC_UI_BASE_URL must be set to the URL of a running osac-ui instance.'); +} +// Diagnostics below never interpolate the raw value — a malformed OSAC_UI_BASE_URL +// can carry embedded credentials or an internal hostname that shouldn't land +// in CI logs. +const invalidBaseURLError = new Error( + 'OSAC_UI_BASE_URL must be an absolute http(s) URL with no embedded username/password.', +); +let parsedBaseURL: URL; +try { + parsedBaseURL = new URL(baseURL); +} catch { + throw invalidBaseURLError; +} +if (parsedBaseURL.protocol !== 'http:' && parsedBaseURL.protocol !== 'https:') { + throw invalidBaseURLError; +} +if (parsedBaseURL.username || parsedBaseURL.password) { + throw invalidBaseURLError; +} + +// Ad hoc specs (typically written by an AI agent to manually verify a change) +// live in this gitignored scratch directory, never under src/ — that keeps +// the "throwaway, not a test suite" boundary obvious to anyone browsing the +// package, instead of relying on everyone remembering src/*.spec.ts is ignored. +const scratchDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'scratch'); +fs.mkdirSync(scratchDir, { recursive: true }); + +export default defineConfig({ + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: 'html', + expect: { + timeout: 15_000, + }, + use: { + baseURL, + trace: 'on-first-retry', + // Dev/lab cluster routes commonly present a self-signed or cluster-internal + // CA cert that Chromium doesn't trust by default (the same reason manual + // testing against these environments always needs curl -k). Opt-in only — + // this flow submits a real Keycloak password, so TLS verification stays on + // by default. + ignoreHTTPSErrors: process.env.IGNORE_HTTPS_ERRORS === 'true', + }, + projects: [ + { + name: 'setup', + testDir: './src', + testMatch: /auth\.setup\.ts/, + // Traces record fill() arguments — never trace the project that types the + // real Keycloak password, even if a future CI run retries it. + use: { ...devices['Desktop Chrome'], trace: 'off' }, + }, + { + name: 'smoke', + testDir: './src', + testMatch: /smoke\.spec\.ts/, + use: { ...devices['Desktop Chrome'], storageState: AUTH_FILE }, + dependencies: ['setup'], + }, + { + name: 'chromium', + testDir: scratchDir, + use: { ...devices['Desktop Chrome'], storageState: AUTH_FILE }, + dependencies: ['setup'], + }, + ], +}); diff --git a/apps/playwright/src/auth-file.ts b/apps/playwright/src/auth-file.ts new file mode 100644 index 00000000..c165f529 --- /dev/null +++ b/apps/playwright/src/auth-file.ts @@ -0,0 +1 @@ +export const AUTH_FILE = '.auth/user.json'; diff --git a/apps/playwright/src/auth.setup.ts b/apps/playwright/src/auth.setup.ts new file mode 100644 index 00000000..08b3c008 --- /dev/null +++ b/apps/playwright/src/auth.setup.ts @@ -0,0 +1,42 @@ +import { expect, test as setup } from '@playwright/test'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { AUTH_FILE } from './auth-file'; + +setup('authenticate', async ({ page }) => { + const username = process.env.OSAC_USERNAME; + const password = process.env.OSAC_PASSWORD; + if (!username || !password) { + throw new Error('OSAC_USERNAME and OSAC_PASSWORD must be set to a valid Keycloak test user.'); + } + + // The app checks /api/login/info on load and, if unauthenticated, immediately + // redirects the browser to Keycloak itself (apps/app-frontend/src/hooks/oidc-login.tsx) + // — there is no login button to click first. This realm's theme is a two-step + // identifier-first flow: username + "Sign In" submits to a second screen with + // the password field, then the same "Sign In" label submits that too. Field/ + // button names match Keycloak's default theme; a custom theme may differ. + await page.goto('/'); + await page.getByLabel('Username or email').fill(username); + await page.getByRole('button', { name: 'Sign In' }).click(); + // getByLabel('Password') is ambiguous — it also matches the theme's "Show + // password" toggle button, which shares the same label association. A + // native input[type=password] has no ARIA role, so getByRole('textbox') + // won't match it either. + await page.locator('input[type="password"]').fill(password); + await page.getByRole('button', { name: 'Sign In' }).click(); + + await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible(); + + // Harden the .auth directory before storageState writes into it — AUTH_FILE + // holds a live, real Keycloak session cookie, and creating the dir with + // default permissions first would briefly expose it before the chmod below. + fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true, mode: 0o700 }); + await page.context().storageState({ path: AUTH_FILE }); + // Restrict to the current user so other local accounts on a shared machine + // can't reuse the session; also re-assert dir perms in case it pre-existed + // with looser permissions. + fs.chmodSync(path.dirname(AUTH_FILE), 0o700); + fs.chmodSync(AUTH_FILE, 0o600); +}); diff --git a/apps/playwright/src/smoke.spec.ts b/apps/playwright/src/smoke.spec.ts new file mode 100644 index 00000000..624bd8d1 --- /dev/null +++ b/apps/playwright/src/smoke.spec.ts @@ -0,0 +1,7 @@ +import { expect, test } from '@playwright/test'; + +test('authenticated app loads and shows the masthead', async ({ page }) => { + await page.goto('/'); + + await expect(page.getByRole('banner')).toBeVisible(); +}); diff --git a/apps/playwright/tsconfig.json b/apps/playwright/tsconfig.json new file mode 100644 index 00000000..88b75dbf --- /dev/null +++ b/apps/playwright/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "playwright.config.ts"] +} diff --git a/package.json b/package.json index 19851fc0..59d0fab2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "lint": "eslint \"{apps,libs}/*/src/**\" && prettier --check \"{apps,libs}/*/src/**/*.{js,jsx,ts,tsx}\" && pnpm run i18n", "format": "eslint --fix \"{apps,libs}/*/src/**/*.{ts,tsx}\" && prettier --write \"{apps,libs}/*/src/**/*.{js,jsx,ts,tsx}\"", "test": "pnpm -r run test", - "typecheck": "pnpm -r run typecheck" + "typecheck": "pnpm -r run typecheck", + "playwright": "pnpm --filter @osac/playwright run playwright", + "playwright:setup": "pnpm --filter @osac/playwright run playwright:setup", + "playwright:run": "pnpm --filter @osac/playwright run playwright:run", + "playwright:report": "pnpm --filter @osac/playwright exec playwright show-report" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53ad77ed..b6d3e4ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,18 @@ importers: specifier: ^4.1.9 version: 4.1.9(@types/node@24.12.2)(@vitest/browser-playwright@4.1.9)(jsdom@26.1.0)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + apps/playwright: + devDependencies: + '@playwright/test': + specifier: ^1.62.0 + version: 1.62.0 + '@types/node': + specifier: ^20.19.41 + version: 20.19.43 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + libs/i18n: devDependencies: i18next-cli: @@ -917,6 +929,11 @@ packages: '@patternfly/react-tokens@6.5.1': resolution: {integrity: sha512-zwepLsIQTL0Lf4R2/PIBOk1m+pm0hYVT3lktf2H4+Y87eRIifwMRb19c+pr4hj4ckGvHs+WxwjTfTj2Qqwn5rw==} + '@playwright/test@1.62.0': + resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + engines: {node: '>=20'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1205,6 +1222,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/node@24.12.2': resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} @@ -2634,14 +2654,14 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - playwright-core@1.61.1: - resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} - engines: {node: '>=18'} + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} hasBin: true - playwright@1.61.1: - resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} - engines: {node: '>=18'} + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} hasBin: true pngjs@7.0.0: @@ -3111,6 +3131,9 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -4103,6 +4126,10 @@ snapshots: '@patternfly/react-tokens@6.5.1': {} + '@playwright/test@1.62.0': + dependencies: + playwright: 1.62.0 + '@polka/url@1.0.0-next.29': optional: true @@ -4317,6 +4344,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 @@ -4426,11 +4457,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/browser-playwright@4.1.9(playwright@1.61.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.9)': + '@vitest/browser-playwright@4.1.9(playwright@1.62.0)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.9)': dependencies: '@vitest/browser': 4.1.9(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.9) '@vitest/mocker': 4.1.9(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) - playwright: 1.61.1 + playwright: 1.62.0 tinyrainbow: 3.1.0 vitest: 4.1.9(@types/node@24.12.2)(@vitest/browser-playwright@4.1.9)(jsdom@26.1.0)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -5971,15 +6002,13 @@ snapshots: picomatch@4.0.4: {} - playwright-core@1.61.1: - optional: true + playwright-core@1.62.0: {} - playwright@1.61.1: + playwright@1.62.0: dependencies: - playwright-core: 1.61.1 + playwright-core: 1.62.0 optionalDependencies: fsevents: 2.3.2 - optional: true pngjs@7.0.0: optional: true @@ -6485,6 +6514,8 @@ snapshots: undefsafe@2.0.5: {} + undici-types@6.21.0: {} + undici-types@7.16.0: optional: true @@ -6792,7 +6823,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.2 - '@vitest/browser-playwright': 4.1.9(playwright@1.61.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/browser-playwright': 4.1.9(playwright@1.62.0)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.9) jsdom: 26.1.0 transitivePeerDependencies: - msw