Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/tiny-oranges-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"wrangler": minor
---

Add `bindingOverrides` and `getExport()` to `createTestHarness()`

Test harness workers loaded from Wrangler config files can now replace a configured binding with a Worker in the same harness. This is useful for replacing platform bindings with test Workers while keeping the source Worker config production-like. Worker handles also expose `getExport()` for calling JSRPC methods on the default Worker export, including mock Workers used as override targets.

```ts
const server = createTestHarness({
workers: [
{
configPath: "./workers/app/wrangler.jsonc",
bindingOverrides: { AI: "mock-ai" },
},
{
config: {
name: "mock-ai",
main: "./workers/mock-ai.ts",
compatibility_date: "2026-06-18",
},
},
],
});

const mockAi = await server
.getWorker<WebEnv, typeof import("./workers/mock-ai")>("mock-ai")
.getExport();
```
35 changes: 21 additions & 14 deletions fixtures/create-test-harness-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ For the testing patterns demonstrated here, refer to the [`createTestHarness()`

## Example app

The app has two Workers:
The app has three 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. |
| Worker | Route | Role |
| -------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `web-worker` | `example.com/*` | Handles user-facing routes, calls the API Worker over a service binding, and uses Browser Rendering to generate previews. |
| `api-worker` | `api.example.com/v1/*` | Fetches upstream user data, caches it in KV, and generates scheduled reports. |
| `mock-browser` | none | Test-only Worker used to replace the Web Worker's Browser Rendering binding. |

## What this fixture covers

Expand All @@ -20,6 +21,8 @@ The app has two Workers:
- Route dispatch across multiple Workers.
- Direct Worker calls with `server.getWorker(name)`.
- Scheduled handler dispatch.
- Test-only binding overrides for platform bindings.
- Calling a Worker's default export with `server.getWorker(name).getExport()`.
- Outbound request mocking with MSW.
- Local storage reset between tests.
- Debug output with `server.debug()`.
Expand All @@ -42,15 +45,19 @@ 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. |
| 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 with service and Browser Rendering binding logic. |
| [`workers/web/wrangler.jsonc`](workers/web/wrangler.jsonc) | Web Worker config. |
| [`workers/web/worker-configuration.d.ts`](workers/web/worker-configuration.d.ts) | Generated Web Worker types. |
| [`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. |
| [`workers/api/worker-configuration.d.ts`](workers/api/worker-configuration.d.ts) | Generated API Worker types. |
| [`workers/mock-browser/index.ts`](workers/mock-browser/index.ts) | Test-only mock Worker for the Browser Rendering binding. |
| [`workers/mock-browser/wrangler.jsonc`](workers/mock-browser/wrangler.jsonc) | Mock Browser Worker config. |

## Related docs

Expand Down
3 changes: 3 additions & 0 deletions fixtures/create-test-harness-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"description": "Integration tests with the createTestHarness() API",
"type": "module",
"scripts": {
"cf-typegen": "pnpm typegen:web && pnpm typegen:api",
"typegen:web": "wrangler types ./workers/web/worker-configuration.d.ts -c ./workers/web/wrangler.jsonc -c ./workers/api/wrangler.jsonc --env-interface WebEnv --no-include-runtime",
"typegen:api": "wrangler types ./workers/api/worker-configuration.d.ts -c ./workers/api/wrangler.jsonc --env-interface ApiEnv --no-include-runtime",
"check:type": "tsc",
"build": "vite build",
"playwright:install": "pnpm playwright install chromium",
Expand Down
9 changes: 7 additions & 2 deletions fixtures/create-test-harness-example/tests/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
"extends": "@cloudflare/workers-tsconfig/tsconfig.json",
"compilerOptions": {
"module": "esnext",
"types": ["node"]
"noEmit": true,
"types": ["@cloudflare/workers-types/latest", "node"]
},
"include": ["../playwright.config.ts", "**/*.ts"],
"include": [
"../playwright.config.ts",
"../workers/*/worker-configuration.d.ts",
"**/*.ts"
],
"exclude": []
}
40 changes: 37 additions & 3 deletions fixtures/create-test-harness-example/tests/vitest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@ 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/web/wrangler.jsonc",
bindingOverrides: { BROWSER: "mock-browser" },
},
{ configPath: "./workers/api/wrangler.jsonc" },
{ configPath: "./workers/mock-browser/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");
const webWorker = server.getWorker<
WebEnv,
typeof import("../workers/web/index")
>("web-worker");
const apiWorker = server.getWorker<
ApiEnv,
typeof import("../workers/api/index")
>("api-worker");
const mockBrowserWorker = server.getWorker<
unknown,
typeof import("../workers/mock-browser/index")
>("mock-browser");

// Workers started by createTestHarness route outbound fetches to globalThis.fetch().
// You can use libraries like MSW to intercept those requests.
Expand Down Expand Up @@ -56,6 +70,26 @@ describe("createTestHarness: Vitest setup", () => {
});
});

test("overrides a platform binding with a mock Worker", async ({
expect,
}) => {
const apiEnv = await apiWorker.getEnv();
await apiEnv.STORE.put(
"daily-report/2026-05-29",
JSON.stringify(["123", "456"])
);

const stubPng = Uint8Array.from([
137, 80, 78, 71, 13, 10, 26, 10, 109, 111, 99, 107,
]);
const mockBrowser = await mockBrowserWorker.getExport();
await mockBrowser.setScreenshot(Array.from(stubPng));

const response = await webWorker.fetch("/reports/2026-05-29.png");
expect(response.headers.get("content-type")).toBe("image/png");
expect(await response.bytes()).toEqual(stubPng);
});

test("dispatches requests using configured routes", async ({ expect }) => {
network.use(
http.get("http://identity.example.com/profile/:id", ({ params }) => {
Expand Down
2 changes: 1 addition & 1 deletion fixtures/create-test-harness-example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"target": "esnext",
"strict": true,
"noEmit": true,
"types": ["@cloudflare/workers-types", "node"],
"types": ["@cloudflare/workers-types/latest", "node"],
"lib": ["esnext"],
"skipLibCheck": true
},
Expand Down
5 changes: 4 additions & 1 deletion fixtures/create-test-harness-example/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export default defineConfig({
plugins: [
cloudflare({
configPath: "./workers/web/wrangler.jsonc",
auxiliaryWorkers: [{ configPath: "./workers/api/wrangler.jsonc" }],
auxiliaryWorkers: [
{ configPath: "./workers/api/wrangler.jsonc" },
{ configPath: "./workers/mock-browser/wrangler.jsonc" },
],
inspectorPort: false,
persistState: false,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --config=./workers/api/wrangler.jsonc --include-runtime=false --env-interface=ApiEnv ./workers/api/worker-configuration.d.ts` (hash: 8ea2ccdfc4342d51b6459a3ebe934dda)
interface __BaseEnv_ApiEnv {
STORE: KVNamespace;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./index");
}
interface Env extends __BaseEnv_ApiEnv {}
}
interface ApiEnv extends __BaseEnv_ApiEnv {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { WorkerEntrypoint } from "cloudflare:workers";

let screenshot: Uint8Array | undefined;

export default class MockBrowser extends WorkerEntrypoint {
setScreenshot(bytes: number[]) {
screenshot = Uint8Array.from(bytes);
}

async quickAction(action: "screenshot", options: { html: string }) {
if (screenshot === undefined) {
throw new Error("Mock screenshot has not been configured.");
}

return new Response(screenshot, {
headers: { "content-type": "image/png" },
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "mock-browser",
"main": "index.ts",
"compatibility_date": "2026-06-01",
}
15 changes: 9 additions & 6 deletions fixtures/create-test-harness-example/workers/web/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
type Env = {
API: Service<typeof import("../api").default>;
};

/**
* Web Worker: renders user profiles and reports by calling the API Worker over a service binding.
*/
Expand All @@ -22,18 +18,25 @@ export default {

const reportsPathPrefix = "/reports/";
if (url.pathname.startsWith(reportsPathPrefix)) {
const date = url.pathname.slice(reportsPathPrefix.length);
const date = url.pathname.slice(reportsPathPrefix.length).slice(0, 10); // YYYY-MM-DD
const report = await env.API.getDailyReport(date);

if (report === null) {
return new Response("No report", { status: 404 });
}

if (url.pathname.endsWith(".png")) {
return env.BROWSER.quickAction("screenshot", {
html: `<h1>Daily report (${date}): active users ${report.join(", ")}</h1>`,
viewport: { width: 600, height: 200 },
});
}

return new Response(
`Daily report (${date}): active users ${report.join(", ")}`
);
}

return new Response("Not Found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
} satisfies ExportedHandler<WebEnv>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --config=./workers/web/wrangler.jsonc --config=./workers/api/wrangler.jsonc --include-runtime=false --env-interface=WebEnv ./workers/web/worker-configuration.d.ts` (hash: ba220acc5e7d9da5ae68234db2179e34)
interface __BaseEnv_WebEnv {
BROWSER: BrowserRun;
API: Service<typeof import("../api/index").default>;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./index");
}
interface Env extends __BaseEnv_WebEnv {}
}
interface WebEnv extends __BaseEnv_WebEnv {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"main": "index.ts",
"compatibility_date": "2026-06-01",
"routes": ["example.com/*"],
"browser": { "binding": "BROWSER" },
"services": [{ "binding": "API", "service": "api-worker" }],
}
Loading
Loading