diff --git a/.github/workflows/test-and-check.yml b/.github/workflows/test-and-check.yml index 6880b25887..ad538d90fc 100644 --- a/.github/workflows/test-and-check.yml +++ b/.github/workflows/test-and-check.yml @@ -7,6 +7,10 @@ on: permissions: contents: read +env: + # We run `playwright install` manually instead + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + jobs: add-to-project: if: github.head_ref != 'changeset-release/main' diff --git a/fixtures/create-test-harness-example/.gitignore b/fixtures/create-test-harness-example/.gitignore new file mode 100644 index 0000000000..aaa9103e44 --- /dev/null +++ b/fixtures/create-test-harness-example/.gitignore @@ -0,0 +1,2 @@ +test-results/ +playwright-report/ diff --git a/fixtures/create-test-harness-example/README.md b/fixtures/create-test-harness-example/README.md new file mode 100644 index 0000000000..3af9e77750 --- /dev/null +++ b/fixtures/create-test-harness-example/README.md @@ -0,0 +1,58 @@ +# Integration testing Workers with `createTestHarness()` + +This fixture is the complete runnable example linked from the `createTestHarness()` docs. It shows how to test a realistic multi-Worker app's production build output from different test runners. + +For the testing patterns demonstrated here, refer to the [`createTestHarness()` examples guide](https://developers.cloudflare.com/workers/testing/test-harness/examples/). + +## Example app + +The app has two Workers: + +| Worker | Route | Role | +| ------------ | ---------------------- | ----------------------------------------------------------------------------- | +| `web-worker` | `example.com/*` | Handles user-facing routes and calls the API Worker over a service binding. | +| `api-worker` | `api.example.com/v1/*` | Fetches upstream user data, caches it in KV, and generates scheduled reports. | + +## What this fixture covers + +- Setup [Vitest](https://vitest.dev/) against Workers developed with Wrangler. +- Setup [Playwright](https://playwright.dev/) against Workers built by the Cloudflare Vite plugin. +- Route dispatch across multiple Workers. +- Direct Worker calls with `server.getWorker(name)`. +- Scheduled handler dispatch. +- Outbound request mocking with MSW. +- Local storage reset between tests. +- Debug output with `server.debug()`. +- Runtime log assertions with `server.getLogs()` and `server.clearLogs()`. + +## Run this example + +To build the packages, run: + +```sh +pnpm build --filter @fixture/create-test-harness-example +``` + +Then run the tests with: + +```sh +pnpm --filter @fixture/create-test-harness-example test:vitest +pnpm --filter @fixture/create-test-harness-example test:playwright +``` + +## Files + +| File | Purpose | +| ---------------------------------------------------------- | ------------------------------------------- | +| [`tests/vitest.test.ts`](tests/vitest.test.ts) | Example for testing with Vitest. | +| [`tests/playwright.test.ts`](tests/playwright.test.ts) | Example for testing with Playwright. | +| [`vite.config.ts`](vite.config.ts) | Builds Vite-generated Worker configs. | +| [`workers/web/index.ts`](workers/web/index.ts) | User-facing Worker. | +| [`workers/web/wrangler.jsonc`](workers/web/wrangler.jsonc) | Web Worker config. | +| [`workers/api/index.ts`](workers/api/index.ts) | API Worker with KV and scheduled job logic. | +| [`workers/api/wrangler.jsonc`](workers/api/wrangler.jsonc) | API Worker config. | + +## Related docs + +- [`createTestHarness()` guide](https://developers.cloudflare.com/workers/testing/test-harness/) +- [`createTestHarness()` API reference](https://developers.cloudflare.com/workers/wrangler/api/#createtestharness) diff --git a/fixtures/create-test-harness-example/package.json b/fixtures/create-test-harness-example/package.json new file mode 100644 index 0000000000..91c2bd1164 --- /dev/null +++ b/fixtures/create-test-harness-example/package.json @@ -0,0 +1,32 @@ +{ + "name": "@fixture/create-test-harness-example", + "private": true, + "description": "Integration tests with the createTestHarness() API", + "type": "module", + "scripts": { + "check:type": "tsc", + "build": "vite build", + "playwright:install": "pnpm playwright install chromium", + "pretest:ci": "pnpm playwright:install", + "pretest:playwright": "pnpm playwright:install", + "test:playwright": "playwright test", + "test:vitest": "vitest run", + "test:ci": "vitest run && playwright test", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@playwright/test": "catalog:default", + "@types/node": "catalog:default", + "msw": "catalog:default", + "typescript": "catalog:default", + "vite": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/fixtures/create-test-harness-example/playwright.config.ts b/fixtures/create-test-harness-example/playwright.config.ts new file mode 100644 index 0000000000..68eaf0a898 --- /dev/null +++ b/fixtures/create-test-harness-example/playwright.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + testMatch: "playwright.test.ts", + use: { + browserName: "chromium", + }, +}); diff --git a/fixtures/create-test-harness-example/tests/playwright.test.ts b/fixtures/create-test-harness-example/tests/playwright.test.ts new file mode 100644 index 0000000000..d98f815f56 --- /dev/null +++ b/fixtures/create-test-harness-example/tests/playwright.test.ts @@ -0,0 +1,105 @@ +import { test as base, expect } from "@playwright/test"; +import { http, HttpResponse } from "msw"; +import { setupServer, type SetupServerApi } from "msw/node"; +import { createTestHarness, type TestHarness } from "wrangler"; + +type TestFixtures = { + reset: void; +}; + +type WorkerFixtures = { + network: SetupServerApi; + server: TestHarness; +}; + +const test = base.extend({ + network: [ + async ({}, use) => { + const network = setupServer(); + network.listen({ onUnhandledRequest: "error" }); + + await use(network); + + network.close(); + }, + { scope: "worker" }, + ], + + server: [ + async ({}, use) => { + const server = createTestHarness({ + workers: [ + // Playwright tests run against the Vite build output. + { configPath: "./dist/web_worker/wrangler.json" }, + { configPath: "./dist/api_worker/wrangler.json" }, + ], + }); + + await server.listen(); + + await use(server); + + await server.close(); + }, + { scope: "worker" }, + ], + + baseURL: async ({ server }, use) => { + const { url } = await server.listen(); + await use(url.href); + }, + + reset: [ + async ({ network, server }, use, testInfo) => { + await use(); + + if (testInfo.status !== testInfo.expectedStatus) { + server.debug(); + } + + network.resetHandlers(); + await server.reset(); + }, + { auto: true }, + ], +}); + +test.describe("createTestHarness: Playwright setup", () => { + test("renders a user profile", async ({ page, network }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + await page.goto("/users/123"); + + await expect(page.getByText("Profile: Ada")).toBeVisible(); + }); + + test("renders a report generated by a scheduled handler", async ({ + page, + network, + server, + }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + const apiWorker = server.getWorker("api-worker"); + await apiWorker.fetch("/v1/users/123"); + await apiWorker.fetch("/v1/users/456"); + await apiWorker.scheduled({ + cron: "0 0 * * *", + scheduledTime: new Date("2026-05-29T00:00:00.000Z"), + }); + + await page.goto("/reports/2026-05-29"); + + await expect( + page.getByText("Daily report (2026-05-29): active users 123, 456") + ).toBeVisible(); + }); +}); diff --git a/fixtures/create-test-harness-example/tests/tsconfig.json b/fixtures/create-test-harness-example/tests/tsconfig.json new file mode 100644 index 0000000000..f1c3da2a58 --- /dev/null +++ b/fixtures/create-test-harness-example/tests/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"] + }, + "include": ["../playwright.config.ts", "**/*.ts"], + "exclude": [] +} diff --git a/fixtures/create-test-harness-example/tests/vitest.test.ts b/fixtures/create-test-harness-example/tests/vitest.test.ts new file mode 100644 index 0000000000..580120c17b --- /dev/null +++ b/fixtures/create-test-harness-example/tests/vitest.test.ts @@ -0,0 +1,114 @@ +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; +import { afterAll, afterEach, beforeAll, describe, test } from "vitest"; +import { createTestHarness } from "wrangler"; + +// Point each worker to the Wrangler config you want to test. +const server = createTestHarness({ + workers: [ + { configPath: "./workers/web/wrangler.jsonc" }, + { configPath: "./workers/api/wrangler.jsonc" }, + ], +}); + +// server.getWorker() would return the first Worker, but naming it makes it explicit. +const webWorker = server.getWorker("web-worker"); +const apiWorker = server.getWorker("api-worker"); + +// Workers started by createTestHarness route outbound fetches to globalThis.fetch(). +// You can use libraries like MSW to intercept those requests. +const network = setupServer(); + +describe("createTestHarness: Vitest setup", () => { + beforeAll(async () => { + network.listen({ onUnhandledRequest: "error" }); + await server.listen(); + }); + + afterAll(async () => { + network.close(); + await server.close(); + }); + + afterEach(async () => { + // Keep tests isolated while reusing the same running server. + network.resetHandlers(); + await server.reset(); + }); + + test("fetches the primary Worker with a relative URL", async ({ expect }) => { + // Relative URLs are dispatched to the primary Worker (the first one listed). + const response = await server.fetch("/"); + expect(await response.text()).toBe("Hello World"); + }); + + test("mocks outbound requests", async ({ expect }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + const userResponse = await apiWorker.fetch("/v1/users/123"); + expect(await userResponse.json()).toEqual({ + id: "123", + name: "Ada", + }); + }); + + test("dispatches requests using configured routes", async ({ expect }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + // server.fetch() matches requests to workers based on routes. + const apiResponse = await server.fetch( + "http://api.example.com/v1/users/123" + ); + expect(await apiResponse.json()).toEqual({ + id: "123", + name: "Ada", + }); + + const webResponse = await server.fetch("http://example.com/users/123"); + expect(await webResponse.text()).toBe("Profile: Ada"); + }); + + test("runs scheduled jobs and stores the result", async ({ expect }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + // Seed user data that the scheduled job will read to generate a report. + await apiWorker.fetch("/v1/users/123"); + await apiWorker.fetch("/v1/users/456"); + + const initialResponse = await webWorker.fetch("/reports/2026-05-29"); + expect(initialResponse.status).toBe(404); + expect(await initialResponse.text()).toBe("No report"); + + server.clearLogs(); + + expect( + await apiWorker.scheduled({ + cron: "0 0 * * *", + scheduledTime: new Date("2026-05-29T00:00:00.000Z"), + }) + ).toEqual({ outcome: "ok", noRetry: false }); + expect(server.getLogs()).toContainEqual( + expect.objectContaining({ + level: "info", + message: "Generated daily report for 2026-05-29", + }) + ); + + const webResponse = await webWorker.fetch("/reports/2026-05-29"); + expect(await webResponse.text()).toBe( + "Daily report (2026-05-29): active users 123, 456" + ); + }); +}); diff --git a/fixtures/create-test-harness-example/tsconfig.json b/fixtures/create-test-harness-example/tsconfig.json new file mode 100644 index 0000000000..e8e338a5dd --- /dev/null +++ b/fixtures/create-test-harness-example/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "target": "esnext", + "strict": true, + "noEmit": true, + "types": ["@cloudflare/workers-types", "node"], + "lib": ["esnext"], + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/create-test-harness-example/turbo.json b/fixtures/create-test-harness-example/turbo.json new file mode 100644 index 0000000000..6556dcf3e5 --- /dev/null +++ b/fixtures/create-test-harness-example/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/create-test-harness-example/vite.config.ts b/fixtures/create-test-harness-example/vite.config.ts new file mode 100644 index 0000000000..71b9f5a29a --- /dev/null +++ b/fixtures/create-test-harness-example/vite.config.ts @@ -0,0 +1,13 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ + configPath: "./workers/web/wrangler.jsonc", + auxiliaryWorkers: [{ configPath: "./workers/api/wrangler.jsonc" }], + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/fixtures/create-test-harness-example/vitest.config.mts b/fixtures/create-test-harness-example/vitest.config.mts new file mode 100644 index 0000000000..61bbfb02b9 --- /dev/null +++ b/fixtures/create-test-harness-example/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + include: ["tests/vitest.test.ts"], + }, + }) +); diff --git a/fixtures/create-test-harness-example/workers/api/index.ts b/fixtures/create-test-harness-example/workers/api/index.ts new file mode 100644 index 0000000000..a3fc9b0b9c --- /dev/null +++ b/fixtures/create-test-harness-example/workers/api/index.ts @@ -0,0 +1,85 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +type Env = { + STORE: KVNamespace; +}; + +type User = { + id: string; + name: string; +}; + +/** + * API Worker: fetches users from an upstream service, caches them in KV, and generates daily reports on a schedule. + */ +export default class ApiWorker extends WorkerEntrypoint { + async fetch(request: Request) { + const url = new URL(request.url); + const userPathPrefix = "/v1/users/"; + + if (url.pathname.startsWith(userPathPrefix)) { + const userId = url.pathname.slice(userPathPrefix.length); + return Response.json(await this.getUser(userId)); + } + + const reportPathPrefix = "/v1/reports/"; + + if (url.pathname.startsWith(reportPathPrefix)) { + const date = url.pathname.slice(reportPathPrefix.length); + const report = await this.getDailyReport(date); + + if (report === null) { + return Response.json({ error: "No report" }, { status: 404 }); + } + + return Response.json(report); + } + + return new Response("Not Found", { status: 404 }); + } + + async scheduled(event: ScheduledController) { + if (event.cron !== "0 0 * * *") { + throw new Error(`Unexpected cron: ${event.cron}`); + } + + const date = new Date(event.scheduledTime).toISOString().slice(0, 10); + const list = await this.env.STORE.list({ prefix: "user/" }); + const userIds = list.keys.map((key) => key.name.slice("user/".length)); + + await this.env.STORE.put(`daily-report/${date}`, JSON.stringify(userIds), { + expirationTtl: 60 * 60, + }); + console.info(`Generated daily report for ${date}`); + + for (const key of list.keys) { + await this.env.STORE.delete(key.name); + } + } + + async getUser(userId: string): Promise { + const key = `user/${userId}`; + const cachedUser = await this.env.STORE.get(key, { + type: "json", + }); + + if (cachedUser !== null) { + return cachedUser; + } + + const upstreamResponse = await fetch( + `http://identity.example.com/profile/${userId}` + ); + const user = await upstreamResponse.json(); + + await this.env.STORE.put(key, JSON.stringify(user)); + + return user; + } + + async getDailyReport(date: string) { + return await this.env.STORE.get(`daily-report/${date}`, { + type: "json", + }); + } +} diff --git a/fixtures/create-test-harness-example/workers/api/wrangler.jsonc b/fixtures/create-test-harness-example/workers/api/wrangler.jsonc new file mode 100644 index 0000000000..0b47e3414a --- /dev/null +++ b/fixtures/create-test-harness-example/workers/api/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "api-worker", + "main": "index.ts", + "compatibility_date": "2026-06-01", + "routes": ["api.example.com/v1/*"], + "kv_namespaces": [{ "binding": "STORE", "id": "shared-store" }], +} diff --git a/fixtures/create-test-harness-example/workers/web/index.ts b/fixtures/create-test-harness-example/workers/web/index.ts new file mode 100644 index 0000000000..6daceb0388 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/web/index.ts @@ -0,0 +1,39 @@ +type Env = { + API: Service; +}; + +/** + * Web Worker: renders user profiles and reports by calling the API Worker over a service binding. + */ +export default { + async fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname === "/") { + return new Response("Hello World"); + } + + const userPathPrefix = "/users/"; + if (url.pathname.startsWith(userPathPrefix)) { + const userId = url.pathname.slice(userPathPrefix.length); + const { name } = await env.API.getUser(userId); + return new Response(`Profile: ${name}`); + } + + const reportsPathPrefix = "/reports/"; + if (url.pathname.startsWith(reportsPathPrefix)) { + const date = url.pathname.slice(reportsPathPrefix.length); + const report = await env.API.getDailyReport(date); + + if (report === null) { + return new Response("No report", { status: 404 }); + } + + return new Response( + `Daily report (${date}): active users ${report.join(", ")}` + ); + } + + return new Response("Not Found", { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/fixtures/create-test-harness-example/workers/web/wrangler.jsonc b/fixtures/create-test-harness-example/workers/web/wrangler.jsonc new file mode 100644 index 0000000000..510e67b41b --- /dev/null +++ b/fixtures/create-test-harness-example/workers/web/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "web-worker", + "main": "index.ts", + "compatibility_date": "2026-06-01", + "routes": ["example.com/*"], + "services": [{ "binding": "API", "service": "api-worker" }], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9f003a4fc..715b76738a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ catalogs: '@hey-api/openapi-ts': specifier: 0.94.0 version: 0.94.0 + '@playwright/test': + specifier: 1.60.0 + version: 1.60.0 '@vitest/runner': specifier: 4.1.0 version: 4.1.0 @@ -241,6 +244,39 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/create-test-harness-example: + devDependencies: + '@cloudflare/vite-plugin': + specifier: workspace:* + version: link:../../packages/vite-plugin-cloudflare + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../../packages/workers-tsconfig + '@cloudflare/workers-types': + specifier: catalog:default + version: 4.20260623.1 + '@playwright/test': + specifier: catalog:default + version: 1.60.0 + '@types/node': + specifier: 22.15.17 + version: 22.15.17 + msw: + specifier: catalog:default + version: 2.12.4(@types/node@22.15.17)(typescript@5.8.3) + typescript: + specifier: catalog:default + version: 5.8.3 + vite: + specifier: catalog:default + version: 8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1) + vitest: + specifier: catalog:default + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) + wrangler: + specifier: workspace:* + version: link:../../packages/wrangler + fixtures/d1-read-replication-app: devDependencies: '@cloudflare/workers-tsconfig': @@ -7464,6 +7500,11 @@ packages: '@playground/module-resolution-requires@file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires': resolution: {directory: packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires, type: directory} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -11397,6 +11438,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + 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} @@ -13240,6 +13286,11 @@ packages: engines: {node: '>=18'} hasBin: true + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + pngjs@3.4.0: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} @@ -18565,6 +18616,10 @@ snapshots: '@playground/module-resolution-requires@file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires': {} + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -22624,7 +22679,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -22733,6 +22788,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -24509,6 +24567,12 @@ snapshots: playwright-core@1.60.0: {} + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + pngjs@3.4.0: {} pngjs@6.0.0: {} @@ -25567,7 +25631,7 @@ snapshots: ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bb3cb2a5ca..f889f572ee 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -117,6 +117,7 @@ catalog: vite: "8.0.13" "ws": "8.21.0" esbuild: "0.28.1" + "@playwright/test": "1.60.0" playwright-chromium: "1.60.0" "@cloudflare/workers-types": "^4.20260623.1" workerd: "1.20260623.1"